LoginSignup
1
0

More than 1 year has passed since last update.

common-lispで関数のスタイルについて認識してみた

Last updated at Posted at 2022-05-28

common-lispで関数のスタイルについて認識してみた

改訂履歴

  1. @nfunato 様よりご指摘を頂きました。LOOPマクロを利用した関数記述スタイルをconsしました。

はじめに

末尾再帰,継続,アクタ,コルーチン : セマンティックウェブ・ダイアリー

現時点で関数の書き方のスタイルとして、ざっくり4種類を認識しています。LOOPマクロでの関数記述スタイルを追加して、5種類の認識となります。

  1. LOOPマクロ利用の関数
  2. 末尾再帰ではない再帰関数
  3. 末尾再帰の再帰関数
  4. 継続渡し
  5. map系関数の利用

2つの簡単な関数をそれぞれのスタイルで記述してみました。

  1. iota関数 : pythonのrange関数のようなもの。
  2. 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)

継続渡しの挙動を強調するため、

  1. 関数の中で継続を実行せず、継続自体を返しています。
  2. 関数の呼び出し元は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)

それぞれメリット、デメリットがあります。状況に応じて使い分ける感じで!

ありがとうございました。何かのお役に立てれば幸いです。

1
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0