Schemeのマクロをかじってみる

今日の昼休みの雑談で「次に学ぶ言語って何かなぁ」という話題になって、JuliaとかElixirとか色々案が出た中で「やっぱりLisp」という話が出て、「Lispを学ぶならやはりマクロは外せない」って話と「Lispは関数が冒頭に来る記法がしっくりこない、Haskellみたいに$でつなげたらいいのに」という話が出て、それってマクロで実装してみるのにちょうどいい課題じゃーん、と思ったので実装してみました。

Gaucheでどうやってコード中に$が出現しているかをチェックしたらいいのかがわからないので、文字列の"$"を使うことにしました。

gosh> (eq? $ $)
#t
gosh> (eq? (car `($ 1)) $)
#f
gosh> (eqv? (car `($ 1)) $)
#f
gosh> (equal? (car `($ 1)) $)
#f

で、こんな感じのコードを書くと(1 "$" inc "$" print)で2と出力されるようになりました。

(define (strip form)
  (if (= (length form) 1) (car form) form))

(define (f form ret)
  (cond
   ((null? form) (reverse ret))
   ((equal? (car form) "$") `( ,(strip (reverse ret)) ,(strip (f (cdr form) ()))))
   (else (f (cdr form) (append ret `( ,(car form) ) )))))

(define (g form)
   (f (reverse form) ()))

(define-macro (haskell form) (g form))

(define (inc x) (+ x 1))

(haskell (1 "$" inc "$" print))
; -> 2

(haskell (inc 1 "$" print))
; -> 2

1個以上の引数を取る関数とかリストがネストしている場合とかはサポート対象外です。

リストがネストしているケースも遊ぼうと思って中置記法で書かれている式を前置記法に変換してから評価するマクロも作りました。( (1 + 2) * (3 + 4) )が21になります。

(define (f2 form)
  (cond ((not (list? form)) form)
        ((= (length form) 3)
         `(,(cadr form) ,(f2 (car form)) ,(f2 (caddr form))))
        (else form)))

(define-macro (fortran form) (f2 form))

(fortran ((1 + 2) * (3 + 4)))
; -> 21

しかしやってみて思うのは、複雑なことをやらないで「〜はサポート範囲外です」と切り捨てればシンプルなコードで面白いことができるけど、現実的に使おうとすると「現実的に入力されうる構文木」が複雑であるがために複雑な条件分岐が必要になってめんどくさくなるってところですね。