Clojure風の無名関数構文

Arcには角括弧で無名関数を作る構文があります。

例:

> (map [if _ (+ 1 _) 1] '(1 -1 nil))
(2 0 1)

便利そうなのでマクロで模倣して喜んでいたんですが、1引数の関数しか作れないのが不便に感じることもありました。

で、最近Clojureにはその多変数版みたいなものがあるらしいのを知り、取り入れてみることにしました。

方針としては、従来のアンダースコアのみの識別子に加えて、その後に数字が付いているものも有効な変数と見なすようにします。

(define (underscore? stx)
  (and (identifier? stx)
       (let ((s (symbol->string (syntax-e stx))))
         (and (positive? (string-length s))
              (char=? (string-ref s 0) #\_)
              (let ((t (substring s 1)))
                (or (zero? (string-length t))  ; _
                    (string->number t)))))))   ; _[0-9]+

そして、マクロ展開時にコードからアンダースコア変数を抽出し、必要となる位置引数を調べます。この際、最初の引数を _1 に対応させることとします。

これらを数字の順番に並べてラムダのパラメータにすれば完成、となるはずです。

その作業を行うのがこちらです。

(define (pos x)
  (or (string->number
       (substring (symbol->string (syntax-e x)) 1))
      1))

(define (positional-vars vars)
  (let loop ((n 1)
             (l (sort (map (lambda (v)
                             (cons (pos v) v))
                           vars)
                      (lambda (x y)
                        (< (car x) (car y)))))
             (r '()))
    (cond ((null? l)
           ;; append rest parameter
           (append (reverse (map cdr r)) (gensym)))
          ((= (caar l) n)
           (loop (add1 n)
                 (cdr l)
                 (cons (car l) r)))
          (else
           (loop n
                 (cons (cons n (gensym)) l)
                 r)))))

アンダースコア変数が例えば _3 だけ、ということもあり得るので、抜けている変数は gensym で補うようにします。

positional-vars の返り値はドット対で、そのままラムダのパラメータ位置に埋め込まれます。

あまり普段はしない使い方だと思いますが、append の最後の引数が非リストの場合はドット・リストが作られます。ドットの後のシンボルがちょうど残余パラメータの役割を果たしてくれるわけです。

これにより、例えばこんな関数が定義できるようになります。

(define second [values _2])  ; values as identity
> (second 'a 'b 'c)
b
> (call-with-values (lambda () (values 'a 'b 'c))
    second)
b

有りそうで無かった感じがしません?


続き。マクロ本体

(define-syntax (make-brackets-funny stx)
  (syntax-case stx (as)
    ((make-brackets-funny (orig as name))
     (with-syntax ((tmp
                    (string->symbol (format "tmp-~a" (syntax-e #'orig)))))
       (syntax/loc stx
         (begin
           (define-syntax (tmp stx~)
             (syntax-case stx~ ()
               ((tmp . expr)
                (bracketed? stx~)
                (with-syntax ((vars
                               (positional-vars
                                (uniq-ids
                                 (filter underscore?
                                         (stx-filter
                                          (lambda (x)
                                            (or (identifier? x)
                                                (bracketed? x)))
                                          (unbracket #'expr)))))))
                  (syntax/loc stx~
                    (lambda vars (orig . expr)))))
               ((tmp . expr)
                (syntax/loc stx~ (orig . expr)))))
           (provide (rename-out (tmp name)))))))
    ((make-brackets-funny orig)
     (syntax/loc stx
       (make-brackets-funny (orig as orig))))
    ((make-brackets-funny orig . rest)
     (syntax/loc stx
       (begin (make-brackets-funny orig)
              (make-brackets-funny . rest))))))

色々省略しましたが、大枠は以上です。

アンダースコア変数を取り出す際に、ネストされている角括弧構文は探索しないようにするのがポイントです (stx-filterの部分)。

(make-brackets-funny #%app if ...)

のようにして利用します。Arcのようにリーダーに手を入れているわけではないので (そうすべきなのかも知れませんが)、無名関数化したい構文ごとに構文を再定義する必要があるんです。


ファイル:
arcfun.ss mlfun.ss