Common lisp でパイプ

シェルのパイプ機能ってとても便利だ。ruby などでメソッドをドットで繋げていくのも楽しい。ということで Common Lisp にてマクロでやってみた。
最初の引数が初期値で、その後ろに関数の名前(lambda 式でもいい)をずらずらと書いていく。キーワード :collect は多値をリストに変換する。関数適用はデフォルトで funcall が使われていて、キーワード :apply を指定すると次だけ apply が代わりに使われる。途中の値を表示したかったら :tee を挟む。
バグと呼ぶのが適切かどうかは分からないけれど、途中に返り値なしの関数が出てきた場合、次の関数には nil が渡されるようになっている。返り値なしのまま :tee を通ったときにも同じことが起きる。

(defmacro pipe (val &rest rest)
  (do ((res val)
       (elt (car rest) (car r))
       (r (cdr rest) (cdr r)))
      ()
    (cond
      ((eq elt :collect)
       (setq res (list 'multiple-value-list res)))
      ((eq elt :tee)
       (setq res (tee-form res)))
      ((eq elt :apply)
       (setq res (list 'apply `#',(car r) res))
       (setq r (cdr r)))
      ((fname-p elt)
       (setq res (list 'funcall `#',elt res)))
      (t (error "Illegal argument: ~S" elt)))
    (if (null r) (return res))))


(defun tee-form (form)
  `(let ((lst (multiple-value-list ,form)))
     (cond
       ((and (consp lst) (null (cdr lst)))
        (format t "~A~%" (car lst))
        (car lst))
       ((cdr lst)
        (dolist (elt lst)
          (format t "~A~%" elt))
        (apply #'values lst))
       (t (format t "~A~%" "; No value")
          (values)))))


(defun fname-p (elt)
  (or (and (symbolp elt) (fboundp elt))
      (and (consp elt) (eq 'lambda (car elt)))))


使用例:

CL-USER> (pipe 1 1+ / :tee list)
1/2
(1/2)
CL-USER>  (pipe (values 1 2 3)
            :tee
            :collect
            :apply
            (lambda (x y z) (+ x (* 3 y) z)))
1
2
3
10