common-lispで関数のスタイルについて認識してみた
改訂履歴
- @nfunato 様よりご指摘を頂きました。LOOPマクロを利用した関数記述スタイルをconsしました。
はじめに
末尾再帰,継続,アクタ,コルーチン : セマンティックウェブ・ダイアリー
現時点で関数の書き方のスタイルとして、ざっくり4種類を認識しています。LOOPマクロでの関数記述スタイルを追加して、5種類の認識となります。
- LOOPマクロ利用の関数
- 末尾再帰ではない再帰関数
- 末尾再帰の再帰関数
- 継続渡し
- map系関数の利用
2つの簡単な関数をそれぞれのスタイルで記述してみました。
- iota関数 : pythonのrange関数のようなもの。
- 2bai関数 : リストの要素を2倍します。ただしリストの要素が3の倍数の場合は3倍します。
LOOPマクロ利用の関数
LOOPマクロは難解ですが、慣れてしまえばかなり強力なのではないでしょうか。
(defun iota-loop (start stop &optional (step 1))
(loop for i from start below stop by step
collect i)) ; => IOTA-LOOP
(iota-loop 1 5) ; => (1 2 3 4)
(iota-loop 1 10 2) ; => (1 3 5 7 9)
(defun 2bai-loop (list)
(loop as x in list
if (= (mod x 3) 0) collect (* 3 x)
else collect (* 2 x))) ; => 2BAI-LOOP
(2bai-loop (iota-loop 1 5)) ; => (2 4 9 8)
末尾再帰ではない再帰関数
まず関数を書くときはこれ。とにかくわかりやすい。ただ、すぐに末尾再帰に変換してしまいますので、短命です。
(defun iota (start stop &optional (step 1))
(cond ((>= start stop) nil)
(t (cons start (iota (+ start step)
stop
step)))))
; => IOTA
(iota 1 5) ; => (1 2 3 4)
(iota 1 5 2) ; => (1 3)
(defun 2bai (list)
(cond ((null list) nil)
((= (mod (car list) 3) 0) (cons (* 3 (car list))
(2bai (cdr list))))
(t (cons (* 2 (car list))
(2bai (cdr list))))))
; => 2BAI
(2bai (iota 1 5)) ; => (2 4 9 8)
末尾再帰の再帰関数
末尾再帰最適化の恩恵を享受するため、デフォルトこのスタイルになる。 (個人的には一番好き。)
(defun iota-tail (start stop &optional (step 1) (acc nil))
(cond ((>= start stop) acc)
(t (iota-tail (+ start step)
stop
step
(append acc (list start))))))
; => IOTA-TAIL
(iota-tail 1 5) ; => (1 2 3 4)
(iota-tail 1 5 2) ; => (1 3)
(defun 2bai-tail (list &optional (acc nil))
(cond ((null list) acc)
((= (mod (car list) 3) 0)
(2bai-tail (cdr list)
(append acc
(list (* (car list) 3)))))
(t (2bai-tail (cdr list)
(append acc
(list (* (car list) 2)))))))
; => 2BAI-TAIL
(2bai-tail (iota-tail 1 5)) ; => (2 4 9 8)
(trace 2bai-tail) ; => (2BAI-TAIL)
継続渡し (Continuation-passing style)
継続渡しの挙動を強調するため、
- 関数の中で継続を実行せず、継続自体を返しています。
- 関数の呼び出し元はfuncallで、継続を実行します。
(defun iota-cps (cont start stop &optional (step 1))
(cond ((>= start stop) cont)
(t (iota-cps (lambda (x)
(funcall cont (cons start x)))
(+ start step)
stop
step))))
; => IOTA-CPS
(iota-cps #'values 1 5)
;; => #<CLOSURE (LAMBDA (X) :IN IOTA-CPS) {1004896FCB}>
(funcall (iota-cps #'values 1 5) nil) ;; => (1 2 3 4)
(iota-cps #'values 1 5 2)
;; => #<CLOSURE (LAMBDA (X) :IN IOTA-CPS) {1004A5D16B}>
(funcall (iota-cps #'values 1 5 2) nil) ; => (1 3)
(defun 2bai-cps (cont list)
(cond ((null list) cont)
((= (mod (car list) 3) 0)
(2bai-cps (lambda (x)
(funcall cont
(cons (* 3 (car list))
x)))
(cdr list)))
(t (2bai-cps (lambda (x)
(funcall cont
(cons (* 2 (car list))
x)))
(cdr list)))))
; => 2BAI-CPS
(funcall (2bai-cps #'values
(funcall (iota-cps #'values 1 5) nil))
nil) ; => (2 4 9 8)
map系関数の利用
高階関数を積極的に利用して、抽象度を上げがちにするスタイルでしょうか。とてもスッキリと書けます。
mapが処理対象のリストが存在することを前提としているため、iotaは書くことができませんでした。抽象度の低い関数ということでしょうか。
;; (defun iota-hof (start stop &optional (step 1)))
(defun 2bai-hof (list)
(mapcar (lambda (x)
(cond ((= (mod x 3) 0) (* x 3))
(t (* x 2))))
list))
; => 2BAI-HOF
(2bai-hof (iota-tail 1 5)) ; => (2 4 9 8)
2baiは、この規模であれば名前無し関数になるかと思います。
(mapcar (lambda (x)
(cond ((= (mod x 3) 0) (* x 3))
(t (* x 2))))
(iota-tail 1 5))
; => (2 4 9 8)
それぞれメリット、デメリットがあります。状況に応じて使い分ける感じで!
ありがとうございました。何かのお役に立てれば幸いです。