curryは飲み物

概要

LISPでカリー化された関数を定義するマクロを作ってみた.カリー化された関数は,必要な数の引数が渡されればただちに評価されて結果を返すが,引数の数が足りなければ部分適用された関数となる.それもまたカリー化されている.

動機

昨日も一日中椅子に座ってJava書いて……今日も一日中椅子に座ってJava書いて……きっと来週も再来週も一日中椅子に座ってJava書いて……毎日毎日同じ事の繰り返しで,生きてる気がしないんだYO!

簡単なカリー化

定義通りカリー化するだけなら,こんなふうにすればいい.

(defun currying (arg-list &rest body)
  (let* ((args (reverse arg-list))
         (last-arg (car args))
         (other-args (cdr args)))
    (reduce #'(lambda (acc elem) `(lambda (,elem) ,acc)) other-args
            :initial-value `(lambda (,last-arg) ,@body))))

(currying '(a b c) '(+ a b c))
; => (LAMBDA (A) (LAMBDA (B) (LAMBDA (C) (+ A B C))))

(defmacro defun/curry% (name arg-list &body body)
  (let ((first-arg (car arg-list)))
    `(defun ,name (,first-arg)
       ,(apply #'currying (cdr arg-list) body))))

(macroexpand-1 '(defun/curry% sample-fun (a b c) (+ a b c)))
; => (DEFUN SAMPLE-FUN (A) (LAMBDA (B) (LAMBDA (C) (+ A B C))))

ちょっとかっこいいカリー化

簡単なカリー化の問題

呼ぶのが超めんどくさい.

(funcall (funcall (sample-fun 1) 2) 3)

やっぱLISPカッコ多いっすねwwwwwなどと言われてしまう.本来はこのように呼びたい.

(sample-fun 1 2)
; => #<部分適用された関数.あと1つの引数で評価される>

(funcall (sample-fun 1 2) 3)
; => 6

(sample-fun 1 2 3)
;; => 6
部分適用された関数を表すオブジェクトを作る

部分適用された関数は状態を持つ関数であると考える.それは次の状態を持つ.

  • これまで与えられた引数
  • あとどれだけ引数が足りないか
  • 引数が足りた時に呼ばれる関数

構造体で表すとこう.

(defstruct curried
  args-luck
  args
  fun
  )

;; 3引数の関数.与えた引数はなし.
(defvar *curried-fun*
  (make-curried :args-luck 3 :args NIL
                :fun #'(lambda (a b c) (+ 1 2 3))))
部分適用された関数オブジェクトを呼ぶ

この部分適用された関数オブジェクトはただの構造体なので,関数のようには呼べない.呼ぶための関数は次のように書ける.

(defun call-curried (curried-fun args)
  "部分適用された関数と,引数のリストを取る関数"
  (cond
    ;; 引数がなければ,任意の引数を取って関数を呼ぶ関数を返す
    ((null args) (lambda (&rest args) (call-curried curried-fun args)))
    ;; 足りない引数が残り1つなら,関数本体を呼び出す
    ((= 1 (curried-args-luck curried-fun))
     (apply (curried-fun curried-fun) (nreverse (cons (car args) (curried-args curried-fun)))))
    ;; それ以外なら,引数リストに値を一つプッシュして,残り引数のカウントを減らす
    (t (call-curried (make-curried :args-luck (- (curried-args-luck curried-fun) 1)
                                   :args (cons (car args) (curried-args curried-fun))
                                   :fun  (curried-fun curried-fun))
                     (cdr args)))))
マクロでラップする

いちいち構造体を定義するのは難儀だし,引数の数をいちいち数えるのもかったるい.定義時に引数の数はわかるので,マクロでラップできる.

(defmacro defun/currying (name arg-list &body body)
  (let ((args (gensym))
        (curf (gensym)))
    `(defun ,name (&rest ,args)
       (let ((,curf (make-curried :args-luck ,(length arg-list) :args NIL
                                  :fun #'(lambda ,arg-list ,@body))))
         (call-curried ,curf ,args)))))

呼び出すときはこう.

(defun/currying add3 (a b c)
  (+ a b c))

(add3 1 2)
; => #<CLOSURE (LAMBDA (&REST ARGS) :IN CALL-CURRIED) {25B69F85}>

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

(add3 1 2 3)
; => 6

まとめ

curryは飲み物