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

lispでドット表記関数呼び出し

common lisp read macro

一般的なオブジェクト指向言語では、メソッドを、インスタンス(オブジェクト)とメソッドの間にドット(.)をつけた表記で呼び出せることが多い。

# メソッド呼び出し
object.method

# ドットを繋げて、複数のメソッド呼び出し
object.methodA.methodB

lispでコードを書いていると、たまにドット表記で関数を呼び出したくなることがあるので、似たようなことをするリードマクロを書いてみる。

; ※ nlet-acc-revの定義は末尾に掲載
;; 文字列分割: 若干非効率?
(defun split-by-char (chr str)
  (nlet-acc-rev self ((i (1- (length str))) (tmp '()))
    (if (= i -1)
        #1=(accumulate (coerce tmp 'string))
      (if (char= chr #2=(char str i))
          (self (1- i) (progn #1# '()))
        (self (1- i) (cons #2# tmp))))))

;; ドット表記の関数呼び出しを、S式に変換
;; ※'(list 1 2 3).car'の様な、先頭がconsの呼び出しには未対応
;; ※二つ以上の引数を取る関数の呼び出しにも未対応
(defun read-dot-exprs (stream)
  (reduce (lambda (arg fn) `(,fn ,arg))
	  (mapcar #'intern
		  (split-by-char #\. (symbol-name (read stream))))))

;; リードマクロ読み込み
(set-macro-character #\@
  (lambda (stream c)
    (declare (ignore c))
    (read-dot-exprs stream)))

テスト

> (defvar lst '(1 2 3 4 5))

> @lst.car
--> 1

> @lst.cdr.list
--> ((2 3 4 5))

> @lst.rest.rest.second.sqrt
--> 2.0

だいたい、こんなものかな。
ただ、書いてみたのはいいけど、わざわざマクロ文字を一つ割り当てるほど必要ではない気がする。

マクロ定義

上のコード内で使っているnlet-acc-rev(とこれが依存する関数・マクロ)の定義

; ※以下の二つの出典は、確か『LET OVER LAMBDA』
(defun formalize-letargs (args)
  (mapcar (lambda (a) (if (atom a) (list a) a)) args))

(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にaccumulateローカル関数を追加
;; accumulateで(accに)蓄積されたものを、最後にlistとして返す
;; 返されるlist内の要素の順番は、accumulateされた順番の逆(スタック順)
(defmacro nlet-acc-rev (fn-name letargs  &body body)
  (let ((acc (gensym)))
    `(let ((,acc '()))
       (flet ((accumulate (x) (push x ,acc)))
         (nlet ,fn-name ,letargs
           ,@body))
       ,acc)))