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 がエラーを出していますね。
可変数引数やオプショナル引数等は捨てなければいけませんが、関数によってはかなり便利なんじゃないかと思います。