ML っぽいカリー化関数を定義するマクロ

ML とか Haskell のコードを読む時に私がどうしても憧れてしまうのが、自動的にカリー化定義される関数です。

Scheme にもカリー化関数を定義する構文自体は存在します (処理系にもよるでしょうが)。

例えば、このようなラムダ式のネストで定義された関数を

(define add3
  (lambda (a)
    (lambda (b)
      (lambda (c)
        (+ a b c)))))

次のようなスタイルで短く書くことができるんです:

(define (((add3 a) b) c)
  (+ a b c))

でも全然自動的ではないですし、必ず定義した通りに適用しなければいけません:

(((add3 1) 2) 3)

2番目と3番目の引数を同時に与える、とかは出来ないわけです。

そこでちょっと知恵をひねりまして、case-lambda を使って、あらゆる関数適用のパターンに応じたラムダ式をあらかじめ作っておく、という方法を考えてみました:

(define-syntax (fun stx)
  (define (make-cases vars body)
    #`(case-lambda
        #,@(for/list ((i (in-range (length vars))))
             (let-values (((hd tl) (split-at vars (+ i 1))))
               #`(#,hd
                  #,(if (null? tl)
                        body
                        (make-cases tl body)))))))
  (syntax-case stx ()
    ((fun (fn v ...) e ...)
     #`(define fn
         #,(make-cases (syntax->list #'(v ...))
                       #'(begin e ...))))))

これを使うと、

(fun (add3 a b c) (+ a b c))

という定義は以下のように展開されます:

(define add3
  (case-lambda
    ((a)
     (case-lambda
       ((b)
        (case-lambda
          ((c) (begin (+ a b c)))))
       ((b c) (begin (+ a b c)))))
    ((a b)
     (case-lambda
       ((c) (begin (+ a b c)))))
    ((a b c) (begin (+ a b c)))))

コードの重複が多少気になりますが、まぁとりあえず動かしてみましょう。

(((add3 1) 2) 3)
; => 6
((add3 1) 2 3)
; => 6
(add3 1 2 3)
; => 6

上手く行ってますね。

明示的な curry との併用なんかはどうでしょう? MzScheme の scheme/function ライブラリで提供されているものを使います:

((curry add3 1) 2 3)
; => 6
((curry (add3 1) 2) 3)
; => 6

出来ました。

今度は compose プラス多値と組み合わせてみたり

((compose add3 (lambda (x) (values x 2 3)))
 1)
; => 6
((compose (add3 1) (lambda (x) (values x 3)))
 2)
; => 6

もいっちょ

((compose (curry add3 1) values)
 2 3)
; => 6

良い感じです。


[追記]

年を跨いで、より完全なバージョンを作ってみました。カリー化された無名関数を作るマクロ fn を加え、ライブラリとしての体裁も整えました:

#lang scheme/base

(require (for-syntax scheme/base
                     (only-in srfi/1 iota split-at)))

(provide fun fn)

(define-for-syntax (make-cases vars body)
  (cond ((null? vars) body)
        ((null? (cdr vars))
         #`(lambda #,vars #,body))
        (else
         #`(case-lambda
             #,@(map (lambda (i)
                       (let-values (((hd tl) (split-at vars (+ i 1))))
                         #`(#,hd
                            #,(make-cases tl body))))
                     (iota (length vars)))))))

(define-syntax (fn stx)
  (syntax-case stx ()
    ((fn (v ...) e ...)
     (make-cases (syntax->list #'(v ...))
                 #'(begin e ...)))))

(define-syntax fun
  (syntax-rules ()
    ((fun (f v ...) e ...)
     (define f (fn (v ...) e ...)))))

バージョン 4 以降の PLT Scheme では、マクロ展開のフェーズは実行時とは別環境になるので注意が必要です。例えばマクロ展開時にライブラリ関数を使いたい場合は (require (for-syntax ... )) として読み込む必要があります。マクロ用に関数を定義したい場合も、define-for-syntax で定義するか、begin-for-syntax で囲んで define するかしなければなりません。

そういう区別が特に無い処理系では define-for-syntax を普通に define に置き換えれば動くと思います。なお、case-lambda は大抵の処理系でプリミティブもしくは srfi-16 で提供されています。

上で言い忘れたんですが、あくまでも ML スタイル (ラムダ算法においてもそうですが、「全ての関数は1引数関数である」というもの) の模倣ですので、Lisp 的なオプショナル引数やキーワード引数、可変数引数はサポートしません。

マクロの定義上、零引数の関数も作れるようになっていますが、その場合は

(fn () 1)
; => 1

関数ではなく定数が返るようになっています。これは定数を零引数関数と見なす数学の考え方とも一致します。


[追記2]

fn を用いて、既存の関数をカリー化するマクロを作ってみました:

(define-syntax (make-curried stx)
  (syntax-case stx ()
    ((make-curried (f n) ...)
     #`(begin
         #,@(map (lambda (f n)
                   #`(define
                       #,(datum->syntax
                          f
                          (string->symbol (format "~a." (syntax-e f))))
                       #,(let ((args
                                (map (lambda (_) (gensym))
                                     (iota (syntax-e n)))))
                           #`(fn #,args (#,f #,@args)))))
                 (syntax->list #'(f ...))
                 (syntax->list #'(n ...)))))))

こういう風に、関数名と引数の数のペアを指定する方式です (複数可):

(make-curried (map 2)
              (+ 2))

(define map-add1 (map. (+. 1)))
(map-add1 '(1 2 3))
; => (2 3 4)

(元の関数と同名で定義したかったんですが、再バインドしようとするとエラーになってしまうのであきらめました)

元の関数の arity はどうあれ、指定した個数の引数しか受け取れない関数が作られます:

(+. 1 2)
; => 3

(+. 1 2 3)
; => procedure +.: no clause matching 3 arguments: 1 2 3

case-lambda がエラーを出していますね。

可変数引数やオプショナル引数等は捨てなければいけませんが、関数によってはかなり便利なんじゃないかと思います。