generated at
S式の尖った特徴を少し削って書き心地を得る
自作のプログラミング言語llrlはS式を構文 (シンタックス) に全面採用している。S式はパースが容易で、階層構造が視覚的にわかりやすいのもあるが、構文の構成要素が少ないことからマクロ機能とも相性が良い。実際llrlでは lambda のような原始的な機能もマクロとして定義されていたりする。
S式の特徴、階層構造がそのまま構文に対応するということは、S式をプログラミング言語の構文に用いる上では欠点にもなる。 例えば let 式のような、新たなスコープを導入するフォームのたび、視覚的にもネストが深くなる。
example1.lisp
(let1 foo (read-string) (println! foo) (let1 bar (read-string) (println! bar) (let1 op (read-string) ...)))

let* のような let のバリエーションはあれど、「変数束縛、分岐、変数束縛」のようなシーケンスではやはりネストが必要だったり、あるいはシーケンシャルに書けても見た目が奇妙になったりする。
example2.lisp
(let* ([foo (read-string)] [_ (println! foo)] [bar (read-string)] [_ (println! bar)] [op (read-string)]) ...)
また let let* などの右揃えもインデント幅が大きく、個人的にあまり好きでない。これはスタイルガイド次第だが…

…とここまでS式である限り逃れられないことかのように書いてきたが、スコープと構文は一致しなくても成り立つ。Lisp系言語で共通する形式から離れ、以下のように閉じた let で変数宣言できる言語を設計することは可能だろう。
example3.lisp
(begin (let foo (read-string)) (println! foo) (let bar (read-string)) (println! bar) (let op (read-string)) ...)
しかしこれは、スコープがどこで導入されるかわかりづらかったり、 begin が必要だったり必要でなかったり (CやJava系の構文の言語では、ブロック {} がスコープと対応するのでわかりやすい)、マクロが (let foo (read-string)) のような式を返したときにエラーとするべきかどうか考えることになったり、色々と嫌な臭いがする。


共通するパターンを見出す
S式のネストが深くなる式を見ていると、ある共通するパターンが見えてくる。リストの最後の要素でネストが深くなり、それが尾のように伸びていくパターンだ。
pattern.lisp
; S式のネストが深くなる式の多くは、リストの最後の要素が伸びている (let1 foo (read-string) (println! foo) (let1 bar (read-string) (println! bar) (let1 op (read-string) ...))) ; こういうパターン...リストの途中の要素が伸びる場合は少ない: (let1 foo (read-string) (let1 bar (read-string) (let1 op (read-string) ...) (println! bar)) (println! foo)) ; なぜなら、ここでbar, opのような変数束縛のスコープを狭める必要がある場合は少ないため、 ; 多くの場合は以下のように記述することができるためだ: (let1 foo (read-string) (let1 bar (read-string) (let1 op (read-string) ... (println! bar) (println! foo))))
このパターンが多いのであれば、このパターンをうまく書き直せるならネストが深くなる場合も少なくなる。

このパターンを見て思い当たるものの一つにHaskellの $ 演算子がある。 $ 演算子はただ関数適用するだけの中置演算子 ( f $ x = f x ) だが、演算子の優先順位が低いので以下のように使える:
example.hs
-- 以下のような関数適用のネストを... hello = foo a (bar b (baz c (hoge fuga))) -- 以下のように記述できる helo = foo a $ bar b $ baz c $ hoge fuga
Lisp系言語では通常、中置演算子は存在しないが、演算子でなくともこのような働きをする構文糖衣があれば、上のようなパターンをうまく書き直せるかもしれない。


@構文の導入
ということでllrlに @ を用いる構文を導入した。 @ 構文はリストの中で用いられ、 @ の後に続く要素を括弧で囲む働きをする。例えば以下のように:
at-syntax.llrl.lisp
(a b @ c d) ;=> (a b (c d)) (a b @ c d @ e f) ;=> (a b (c d (e f)))

これによって、冒頭のプログラムを以下のように書き直すことができる。
re.example1.llrl.lisp
(begin @let1 foo (read-string) (println! foo) @let1 bar (read-string) (println! bar) @let1 op (read-string) ...)
※視覚的な統一感のため冒頭に begin を用いているが、 (let1 foo ...) で始めてもよい

この構文は、S式の「データの階層構造が視覚的に見える構文の構造と直接対応する」という特徴を削るが、そのほかのS式の特徴は損なわないし、言語のその他の部分にもなんら変更を必要としない。アドホックな構文の追加ながら、少ない手間で書き心地が非常に良くなり、なかなか気に入った言語機能となったので書き記しておいた。

もちろん @ let1 以外にも用いることができて、例えばllrlの examples/aobench.llrl 内の関数を例に取ると、 let1 式の他にもパターンマッチを行う with1 式などを @ で平坦にすることで、以下のように書ける。
mandelbrot.llrl.lisp
; このような関数を... (function (ray-plane-intersect isect ray plane) {(-> (Ref Isect) Ray Plane unit)} (let* ([d (- (vdot (.p plane) (.n plane)))] [v (vdot (.dir ray) (.n plane))]) (when (< (abs v) 1.0e-17) (return)) (let1 s (/ (+ (vdot (.org ray) (.n plane)) d) -1 v) (with1 (isect: (let t) _ _ _) ~isect (when (< 0.0 s t) (let1 p (v+ (.org ray) (v* (.dir ray) s)) (set! isect (isect: s p (.n plane) #t)))))))) ; このように書き直せる (function (ray-plane-intersect isect ray plane) {(-> (Ref Isect) Ray Plane unit)} @let1 d (- (vdot (.p plane) (.n plane))) @let1 v (vdot (.dir ray) (.n plane)) (when (< (abs v) 1.0e-17) (return)) @let1 s (/ (+ (vdot (.org ray) (.n plane)) d) -1 v) @with1 (isect: (let t) _ _ _) ~isect (when (< 0.0 s t) @let1 p (v+ (.org ray) (v* (.dir ray) s)) (set! isect (isect: s p (.n plane) #t))))

@ を用いたのは、 @ が構文割り当てがなく空いているASCII characterだったからだが、準クォートの ,@ のsplicingとは逆に括弧を補う形になっていて、対になってて悪くないかと思う。