読者です 読者をやめる 読者になる 読者になる

nlet-acc

common lisp utility

良く使うユーティリティ関数・マクロの定義などを書くことにする。*1
一つ目はこれ。

【定義】
(defmacro nlet-acc (fn-name letargs &body body)
  (let (acc (gensym))
    `(let ((,acc '()))                         ;  acc: 結果蓄積用のlist  ※1
       (flet ((accumulate (x) (push x ,acc)))  ;  accmulate: 結果(x)を蓄積する関数
         (nlet ,fn-name ,letargs
           ,@body))
       (nreverse ,acc))))                      ;  accは逆順に格納されているので、nreverse

;; ※1 $$などのような名前をつけて、スコープ内でアクセスできるようにするか検討中 
【説明】

局所的な名前付き関数(fn-name)を定義・実行するマクロ。

fn-name関数のlexicalスコープ内では、accumulateという一引数関数が使える。
accumulateに渡した値は(FIFO順で)蓄積され、これがnlet-acc式の結果として返される。

letargsは、fn-name関数の引数リストで、「変数名|\(変数名 初期値\)*」形式のlist。
初期値が省略された場合は、nilが値となる。

【使用例】
;; mapcarの実装(簡易版)
(defun my-mapcar (fn list)
  (nlet-acc self ((list list))
    (unless (endp list)
      (accumulate (funcall fn (car list)))
      (self (cdr list)))))

基本的にmap系の関数を一般化*2したような使い方を意識している。

【依存定義】

以下、定義に必要な関数・マクロ。
出典は『Let Over Lambda』だったと思うが、はっきりは覚えていない。今度読む時に要確認。 ※ 追記: nletは同書のp.45にあった。

;; 引数をlet形式に正規化
;; ex. (a b (c)) -> ((a) (b) (c))
;; こうすると、変数名を(car arg)で、値を(cdr arg)で取得できるようになる。※ letの値のデフォルトはnil
(defun formalize-letargs (args)
  (mapcar (lambda (a) (if (atom a) (list a) a)) args))

;; lambdaに、名前をつける機能(=自分の再帰呼び出し)を追加したようなマクロ。
;; (funcall (lambda (a b c) ...) 1 2 3)  ==>  (nlet ((a 1) (b 2) (c 3)) ...)
(defmacro nlet (fn-name letargs &body body)
  (setf letargs (formalize-letargs letargs))
  `(labels ((,fn-name ,(mapcar #'car letargs)
              ,@body))
     (,fn-name ,@(mapcar #'cadr letargs))))

僕の場合は、以下のようなnlet定義が多かったので、nlet-accを追加することにした。

(let ((acc '()))
  (nlet self (a b c)
    ...
    (push ... acc))
  (nreverse acc))

*1:説明などは結構適当。いつかちゃんとしたい...

*2:対象がsequenceじゃなかったり、一対多(?)のマッピングやフィルターを行いたい時に便利