ちょっとしたテキスト処理や日々の仕事で自分だけが使うコマンドラインツールの作成にScheme……というか、Gaucheはとても便利です。みなさんもそうですよね?
そんなみなさんであれば、きっと常にuse
したくなるオレオレマクロモジュールがあるにちがいありません。濫用するなと厳しく教えられるマクロではありますが、自分しか使わないごく小規模のプログラムであれば気にする必要もないでしょう。オレオレマクロと喧嘩はLISPの華、書くのが楽なほうがよっぽど大事なわけですから。
もちろん、私にもそんなマクロがいくつかあります。「標準のif
をArcライクなif
で上書きする」という影響範囲がめちゃくちゃ大きいやつとかもお気に入りなのですが、今回はそれは置いておいて、Clojureのスレッドマクロの亜種を晒すことにします。
仕様
「亜種」と言ったとおり、実はClojureにあるものそのままではありません。自分が便利に使えるように欲張りな仕様になっています。
- 名前は
->
/->>
であるものの、動作はsome->
/some->>
に近い。つまり、出てきた値によっては評価を途中で打ち切って短絡させる
- オリジナルの
some->
/some->>
はnil
で短絡させているのですが、今回はSchemeなので#f
で短絡させることにします。短絡させないバージョンはありません
- とはいえ「最初の引数限定(
->
)」「最後の引数限定(->>
)」だと不便なこともあるので、(cut
の<>
みたいに)%
で前の値を入れる場所を指定可能にする。ただし、とくに指定しなければ最初/最後に引数とする
- ここは危険なところで、
as->
とかで実現するのが正しいのでしょうが……実際書くときにめんどうなので……
既存の試み
もちろん既存の試みもあります。ここでは総本山たるPractical Schemeにあるyamasushiさんの実装を挙げておきます。
ただ、これだと「デフォルトでは最初/最後に挿入、違うときだけ指定」ができません(逆に今回の実装の場合は、多値については最初から諦めていますが)。
実装
というわけで、2を実現しようといろいろがんばったのですが、自分にはsyntax-rules
だけでは無理でした(健全性を破ってしまうから……だと思うんですが、正しいのかよくわからない。「既存の試み」で任意でないならできているので、こちらもできるのかな……?)。
伝統的なマクロを使ってもよかったのですが、今回はer-macro-transformer
を使いました(書いたのがだいぶ前なので理由を覚えていないのですが、使ってみたかったんだと思います)。つまり、Gauche以外の(Gaucheにしかない各種手続き・構文、特にer-macro-transformer
に対応していない)Scheme処理系では使えません。
(use util.match)
(define-syntax ->
(er-macro-transformer
(^(form rename id=?)
(match form
((_ cur) cur)
((_ cur (and expr (proc . args)) rest ...)
(if (find-deep '% args)
(quasirename rename
(let1 ,'% ,cur (if-let1 next ,expr (-> next ,@rest) #f)))
(quasirename rename
(if-let1 next (,proc ,cur ,@args) (-> next ,@rest) #f))))
((_ cur proc rest ...)
(quasirename rename
(if-let1 next (,proc ,cur) (-> next ,@rest) #f)))))))
(define-syntax ->>
(er-macro-transformer
(^(form rename id=?)
(match form
((_ cur) cur)
((_ cur (and expr (proc . args)) rest ...)
(if (find-deep '% args)
(quasirename rename
(let1 ,'% ,cur (if-let1 next ,expr (->> next ,@rest) #f)))
(quasirename rename
(if-let1 next (,proc ,@args ,cur) (->> next ,@rest) #f))))
((_ cur proc rest ...)
(quasirename rename
(if-let1 next (,proc ,cur) (->> next ,@rest) #f)))))))
(define (find-deep sym tree)
(let/cc return
(let loop ((t tree))
(match t
((x . xs) (begin (loop x) (loop xs)))
(x (if (eq? sym x) (return #t) #f))))))
->>
を例にとってmatch
の中をいちおう解説すると……。
まず、((_ cur) cur)
は終了条件ですね。(->> init)
でinit
をそのまま返してねという部分です。
次は((_ cur (and expr (proc . args)) rest ...)
なのですが、これは(->> init (proc arg1 arg2...) ...)
というふうに、前の値以外の引数をとる手続きの場合です。ちょっとややこしいですね。ここでは、引数のなかに特定のシンボルが含まれるかどうか(find-deep
は、式の中に%
というシンボルがあるかどうかを判定する手続きです)でさらに場合分けしています。
「%
が含まれなかった場合」(elseのほう)については単純です。「前の値を最後の引数に手続きを適用する」「結果が#f
なら短絡して#f
を返す」「そうでないならそのまま再帰」という形になっています。
(if-let1 next (,proc ,@args ,cur)
(->> next ,@rest)
#f)
「%
が含まれる場合」は、出てきた%
を前の値に束縛したうえで、同様のことを行います。ポイントは,'%
でリネームを避けているところです。リネームとはなんなのか、おぼろげにしかわかっていませんが……。
(let1 ,'% ,cur
(if-let1 next ,expr
(->> next ,@rest)
#f))
最後は(_ cur proc rest ...)
です。(->> init proc ...)
みたいに1引数の手続きをポンと渡された場合ですね。上でほかの引数をとる場合を除外できているので、先ほどのelse節部分とほぼ同様ですね。
まとめ
Clojureのスレッドマクロの亜種をExplicit-renamingマクロで実装しました。ドキュメントと顔つき合わせて実装したのですが、正直自信がありません……「そこ間違ってるよ」「もっとこうしたほうがいいよ」といったものがあれば、ぜひ教えていただければと思います!