Lisp:S式の理由
S式は人に優しいか
Shiro: Lispの不人気の理由として筆頭に上げられるのが、括弧だらけの 独特の見た目。とっつきにくい、一般的な表記法と違っていてわかりにくい、 等々、様々なことが言われてきました。しかし、 S式を捨てたLispも開発されましたが 流行ったとはいい難く、Lispな人々はいまだに括弧に固執しているかのようです。
S式のメリットをLisperに尋ねれば、エディタがどうの、マクロがどうの、といった 回答が真っ先に返って来ると思うんですが、そういう理屈をいくら理解しても S式がダメな人がS式を好きになったりはしません。どうも、もっと根本的な 感覚に大きな隔たりがあるような気がします。非Lisperから理解しがたい、 Lisperの持つ感覚とはどんなものなんでしょうか。Lisp脳から見た世界は どんなものなのでしょうか。
構文木を人間が書く?
S式は言ってみれば言語の構文木そのものです。普通の言語では、処理系の フロントエンドにある構文解析器が、「人間に優しい」文法を「機械が理解しやすい」 構文木に変換します。ということは、Lispは機械にやらせれば良い構文解析を わざわざ人間がやっているってことになります。なんで人間がわざわざ 機械に歩み寄らないといけないんでしょう?
Lisperから見ると、この議論は逆なんです。頭の中でアルゴリズムを考える時、 あなたはどう考えますか? プログラムコードで考える? 私は少なくとも、 頭の中にあるのはコードよりももっと抽象的な、木やグラフみたいなものです。 普通の言語で書く時、私は頭の中のグラフを一度わざわざプログラムコードに変換して書き下します。
本当はグラフを直接入出力できれば良いのかもしれません。しかし現在の ユーザインタフェースでは入出力の制限がきつすぎて、グラフを直接 編集するよりはテキストで編集するほうがはるかに効率が良いのが事実です。
S式は最小限の労力で、グラフをテキストにシリアライズする手段です。 なのでLisp脳にとって、S式は人間が機械のために構文解析をやってあげている のではなく、むしろ人間が一度抽象的なグラフを言語に変換する手間を省いているのです。
ならインデントでも良くね?
そうは言っても、S式を改行無しにべたっと書き下したものを 人間が見て木構造を読み取るのは難しいのは事実。現実には適度に インデントをつけることで木構造を読み取り易くしています。
それなら、最初からインデントで木構造を表せばいいじゃん、 という議論は十分に説得力があります。 実際、Schemeではsrfiでそういう構文が提案されたことさえあります。 が、多くの支持を得るには至りませんでした。
srfi-49に関して言えば、その問題点はPer Bothnerのコメントでうまく表現されています。 "その提案は不十分かつ過剰だ。(略)本当に有用にするためにはもっと多くの構文が必要となるはずで、それは非常に大きな仕事になる" という指摘です。
個人的には、良いインデント構文が発明されてエディタが対応すれば 喜んで移ると思います。ただ、この「良い」が曲者です。
多分この問題は、Lisperの臆病さに根ざしているような気がします。 ここで言う臆病さとは「いつだって、予見不可能な問題に出会うことがあり得る」 という警戒心です。 「今理解されている範囲の問題に最適化した構文を導入して、もしそれが 新たに現れた問題に対応できなかった場合、困るじゃないか」、ということを つい考えてしてしまうのがLisp脳の典型的症状なのです。 新たな問題に対応するために互換性のある形で構文を拡張できるだろうか、 あるいはできたとしても、今度は別の問題で別の形で構文を拡張する 必要が出てきて、それと矛盾が生じるかもしれない…などど 悩み出すときりがありません。
S式ならばこの心配はありません。なぜならどんな問題であれ、 それが計算機で処理可能ならば何らかの内部表現を持つはずなので、 新たな問題に対応する内部表現を考えたらそれをそのまま書き下せば良いだけ だからです。
この思考法は構文に限らず、Lisperの言語設計一般に対する見方を 特徴づけているようにも思えます。Lispの好き嫌いがはっきり分かれるのは 実はこの見方のせいかもしれません。
Lisperは、解き方がわかっている問題を楽に解くよりも 未だ見ぬ問題に楽に対応できる方を重要と考えているフシがあります。 どういう問題だかわからないわけですから、今ある言語で十分とは言えません。 むしろ、今ある言語では不十分になることを想定していなければなりません。 なので、言語を拡張できる自由度を最大限にキープしておきたいと思うわけです。
一方、世の中のプログラミングの99%はおそらく既に解き方がわかっている問題を 解くものです。なので、そういう問題に特化した(構文面での)最適化を施した方が、 プログラマ全体としての幸せの総量は大きくなるでしょう。
Lisperは、非常に困難な問題に直面した時にその「早すぎる最適化」が自分の 足を引っ張ることを恐れます。心配性ですね。
前置記法は慣習とかけ離れてる?
この点に関するLisperの感覚も、おそらく前項で挙げた点に深く関わっています。
中置記法を自然に導入するには、中置に使える演算子とその優先順位を 決めておく必要があります。その決め方の根拠になるのは、これまで わかっている問題セットから得られた経験ですが、それが今後現れる 問題にとっても最適であるという保証はありません。そもそも可能な 演算子の種類に対して、コンセンサスが出来ている演算子などごく わずかです。問題の種類によっては整数除算と普通の除算を混ぜて 使いたいかもしれないし、正確な除算と非正確な除算を区別して 使いたい時もあるかもしれない。それぞれの問題に最適な記法がある かもしれないのに、あらかじめ決められた演算子とそれ以外とで 違う記法を強制されるのがいかにも不自由です。
もちろんPrologとかHaskellとか、自由に中置演算子を定義できたり 関数適用形式と演算子形式と両方使えたりするようにするのは 一つの解決法ではありますね。でも自分で好きなようにできるなら、 Lispには既にreader macroで中置記法を部分的に導入するライブラリが いくつもありますし、より普通なシンタックス「も」使えるように なっている処理系もあります (例えばPLT Schemeでは H-expressionという、JavaScriptっぽい文法がS式と混在して書けるみたいです)。
にもかかわらずLisperの中に代替文法をあまり使ってる人がいないのは、 結局 (1)中置記法が自然な数式というものがプログラムに占める割合はそう多くなく、 (2)前置記法と中置記法が混在する混乱よりは全て前置記法の方が わかりやすいと考える人が多い、ってことなのかもしれません。
第一印象がとっつきにくい?
代替構文も含め、ユーザの裾野を広げるために、もうちょっと 見かけをとっつきやすくしようという話はLisp界にもあります。
ただ、Lisp:よくある正解でも述べたようにLisperは身勝手でして、 「言語が自分の足を引っ張らないこと」を最重要視するため、 少しでも足を引っ張る可能性があるならとっつきやすくする変更が 採用されることは無いでしょう。
上で述べた、Lisperの「未知の問題への適応性を最重視する」というマインド からの派生なんですが、Lisperは「よく知らない人間の直観」というものを あまり信用していない気がします。
どんな記法が自然に感じられるかを含めて、言語を深く知らない人が 「自然に」期待する構文やセマンティクスというのは、その人がそれまで 触れて来た他の言語や記法、つまり既知の世界の中での一貫性を保つように 培われてきたものです。
未知の問題に取り組もうと思ったら、まず未知の問題を記述するための 言葉を探すことから始めなければなりません。それが運良くこれまでの 言葉の自然な延長であれば良いのですが ('+' を実数同士の演算から 複素数同士の演算に拡張する、とか)、既知の世界との齟齬をきたす 場合もよくあります ('+' が可換な演算だということに慣れた人に とって、文字列の連結に '+' を使うのは非常に奇妙です。あるいは、 ベクトル同士の '*' は何を意味すべきでしょうか?)。
結局、何が「自然」なのかは、その問題を深く理解するまではわからないし、 その問題を深く理解するためには記述してみなければならない、 従って、「自然な記法」というのをアプリオリに定義することはできない、 というのがLisperの立ち位置のように思えます。
(ちなみに、私自身が最初Lispを見たときどう思ったのかというと、 あまり覚えてません。けれど、APLを見て「かっこい〜」と憧れて 専用のキャラクタROMを焼いたりしてたくちなので、 Lispにそんなに奇異な印象を受けなかった可能性は高いです)
しかし何もわからないのならどうすれば良いのでしょう。 Lisperの戦略は、全てをなるべく単純で直交する規則で表現しておくことです。 規則が複雑になればなるほど、例外規定が多くなればなるほど、 新たな規則を導入する際に矛盾を生じる可能性が多くなるからです。 単純で、直交する規則であれば、新しい問題に対してまずそれらを 組み合わせて対応し、対応できなければ新たな直交する規則を 導入することでそれまでの体系と矛盾なく言語を拡張してゆくことができます。
この立場から逆にLisperにとっての「驚き最小の法則」を考えれば、 それは「ある構文や意味が常に単純で直交した最小限の規則によって 記述されること」となります。
複雑な規則は、見通しの悪いジャングルのようなものです。既にある 道を通っているぶんには便利かもしれませんが、一歩道を外れると どこに危険が潜んでいるかわかりません。Lisperはむしろ、道なんて 無くてよいから見通しの良い平原を期待します。なぜなら、初めて取り組む 問題にはもともと道なんて無いからです。
S式からだいぶ離れてしまいましたが、ここで書いたような感覚の 有無が、Lispの好き嫌いに多少なりとも関連しているように思えます。
Lisperが言語に期待するもの
これまで盛んに「未知の問題」を繰り返してきましたが、 現実問題として、そんなに未知の問題に遭遇するものなのでしょうか。 未知の問題が、普通にプログラマをやってて一生に一度お目にかかるかどうか、 なんてものなんだとしたら、それに向けた備えのことばかり気にするのは 馬鹿げています。
まあ実際のところ、今まで世界で誰もやったことがないような問題には 滅多にお目にかかりません。現実には、問題ごとにちょっとだけノントリビアルな 側面があったりするって程度です。
Lispプログラマとして実際に多く直面するのは、取り組んでいる問題の 記述方法を探っていった結果として、そのアプリケーション領域用に 特化されたミニ言語を実装してしまう、というケースです。 こういうケースは日常的に出会うので、ベースとなる言語に対しては、 自分が書く言語の足を引っ張らないでいてくれることを期待します。
そう、LisperにとってLispはただの言語というより、 自分の言語を書くための言語なんです。 だから最大公約数的に便利な機能であっても、 それが自分の言語の設計の足かせになるなら、 無い方が良いと考えてしまうのです。
ということは、Lispが初心者に優しくないというのは不可避なの かもしれません。Lispはプログラマが言語設計者でもあることを 想定します---提供された機能を使うだけで良いなら、既にLisp並に便利で もっと人気がありライブラリが揃ってる言語がいくつもあるわけなんで、 Lispを敢えて使う必要性はあまり無いからです。
プログラムを処理するプログラム
とは言っても、問題固有のロジックを簡潔に「記述」するだけのDSLなら、 高階関数と少々のシンタックスシュガーがある言語ならそれなりの ことはできます。
LisperにとってのDSLはもう少し含む範囲が広くて、記述だけでなく その言語の効率の良い実装までを考えます。つまり、単なる表記上の 修飾ではなくて、本当の意味で言語処理系を書いてしまうということです。 この時、もとのプログラムがS式であると、処理系の実装が圧倒的に楽なのです。
例えばGaucheのSXMLライブラリは、 もともと様々なSchemeで動作するようにポータブルに書いてあったものを Gaucheが効率良く実行できるように変換して使っていますが、ソースは オリジナルを持っていて、ビルド時に自動で変換をかけています。 また、Gaucheのコンパイラcompile.scmでは実行時のオーバヘッドを 可能な限り削るため、素直に書けばcaseによる比較になる 内部でのツリートラバーサルのディスパッチを、マクロを利用して 整数インデックスによるテーブルのルックアップで行うような コードが生成されるようにしています (手持ちのCコンパイラの、 デフォルトのswitch〜case文の最適化処理に不満があるので、 自分で書き換えてしまった…みたいな感覚に相当します)。
Gaucheの例を出したのは公開されているコードだからで、Gaucheが 特別な例ではありません。プチ言語処理系を書くのはLisperがほとんど 無意識にやっている日常の作業です。 S式からS式へのトランスレータはとても簡単なので、いちいち言語処理系と 構える必要もありません。
Lispにとっつきやすい構文を導入すること自体はやぶさかでは ないのです。ただ、そうやって導入した構文が、プログラム変換器の 書きやすさに干渉してはならない、というのがLisperにとっては譲れない ところなんですね。そして歴史を振り返ると、その要件を満たすような構文を デザインするのは、簡単なことではないようなのです。
S式の限界
LisperがS式から離れない理由の一端を書いてみましたが、 かと言って現在のLisp/Schemeの構文がベストと思っているわけでもありません。 個人的に、現在のLisp/Schemeの構文に感じている違和感も 書いておきましょう。
括弧の意味の多重化
Lispの括弧は特殊形式か関数適用である、というのが大原則なんですが、 Lispにはそれ以外の意味で括弧が使われる場合があります。letの変数の グループ化や、condの節の区切りですね。
個人的には、このような意味の違うものは別に見えていて欲しいです。 処理系によっては [] を () と同じように使えて、多少見かけを 区別することができます:
(let ([x (calculate-x a b c)] [y (calculate-y d e f)]) body ...) (cond [(predicate x) (do-something) (do-something2)] [(predicate y) (do-another-thing)] [else (do-whatever)])
これは論文の表記などでよく使われてきました。SchemeではR6RSでは 正式な規格に昇格しそうです。でもやっぱりアドホックな感じがするんですよねぇ。
構文木をリストで表現する場合、構文木のノードに付加情報を持っておけない (ノードの種別などもリスト中に保持していなければならない)という制約があるため、 S式では複数種類の括弧の区別という情報を直接は扱えません。
個人的には、condは (Paul GrahamがArcで 提案しているように) 括弧をひとつ省いて、(cond 述語 式 述語 式 ...) で いいんじゃないかという気がします。式に複数の処理を書きたければbeginを使うと。
(cond (predicate x) (begin (do-something) (do-something2)) (predicate y) (do-another-thing) else (do-whatever))
また、letについては、束縛とボディの式に別種の括弧を使うとか:
(let [x (calculate-x a b c)] [y (calculate-y d e f)] body ...)
[a ...]をリーダマクロで (%square-bracket a ...) みたいな何らかのS式に 展開してやれば、プログラム変換自体はS式の枠組で処理できます。
ここに踏み切れないのは、やはりこれまでのLispの慣習に対する慣れなのかもしれません。
Apply
関数fooが2つの引数を取るとします。barは1つ引数を取って、 ある定数とその引数をfooに渡すとすれば、こんな風に書けます。
(define (bar x) (foo 0 x))
ここで、fooが2つ「以上」の引数を、barが1つ「以上」の引数を取ることに 変更になったとします。現在のSchemeでは、残りの引数をfooに渡すためには applyを使うことになります。Common Lispでも似たようなものです。
(define (bar x . opts) (apply foo 0 x opts))
これがどうもすっきりしない。barとfooの変更が対称でないのも 気持ち悪いですし、一度fooの前に引き返してapplyを挿入するの面倒。 また、'foo'の呼出し自身が目立たなくなってしまうって感じもします。
(foo 0 x . opts) で余分な引数を渡せる、と書ければ良いのですが、 optsの部分に変数でなく他の関数呼出しが来た場合に問題となります。 S式の表記上の定義から、 (foo 0 x . (get-opts z)) は (foo 0 x get-opts z) と等価で、プログラムからは区別出来ないからです。
Pythonでは *args でしたっけ、何らかの記法を導入することでapply 無しにすっきり書けるようになってましたね。
前置記法の原則をちょっと崩せば、例えば (foo 0 x : opts) という 表記をコンパイラが (apply foo 0 x opts) に読みかえる、といった ハックは可能です。マクロとの相互作用で不便なことが生じないかどうかは さらに検討する必要がありますが。
アクセサ
四則演算の中置記法は別に欲しいとは思わないんですが、 アクセサは短縮記法が欲しいと思うことがよくあります。 Cの x->y->z、あるいはJavaの x.y.z に相当するものが (ref (ref x 'y) 'z) ですからねぇ。[]がR6RSで予約されなかったら Gauche:スロットアクセスに書いていた構文 [x'y'z] を 結構真剣に検討していたんですが。
実際、以前にプチDB query言語をCommon Lispを使って 実装した時は、table.column という表記でテーブルtableの カラムcolumnがアクセスできるようにリーダマクロを使って (-> table column) と読むようにしていました。 かなり快適でした。
てなわけで、S式は唯一無二の解ってわけではありません。 S式のメリットと直交する構文はあり得ると思うんで、 気長に考えてゆきたいと思っています。
参考
WiLiKi内:
外部リンク:
- http://d.hatena.ne.jp/babie/20070118/1169119443 「Lisp が敬遠されてる理由って、はっきりいって syntax だと思うなぁ。」
- http://enbug.tdiary.net/20070118.html#p02 「Lispが大衆受けしない理由は、その表記法の非人間性、あるいは、慣習との齟齬に尽きると感じています。」
- http://d.hatena.ne.jp/harg/20070122/1169456152 「要は、最初に出会ってしまった言語が何かっていう問題じゃないの?」
- http://d.hatena.ne.jp/scinfaxi/20070122/1169452296 「C言語のあの妙な構文に比べればS式の素晴らしさは異常、という感じ」
LispUser.netさんのところに素晴らしい エントリが。 新しいcond構文やlet構文をマクロで実装する様子が動画で見られます。 EmacsのS式編集機能や補完もばりばり使ってるので、Lisperのコード書きの様子が 知りたい人も必見。
議論、コメント
- えんどう(2007/01/23 05:40:17 PST)Lisperは逆ポーランドについてはどう思ってるんでしょうか? リストで脳内スタックを構築して処理してるとか?
- Rui(2007/01/23 07:17:29 PST): S式の語順は(動詞 名詞)となりますが、自然言語で動詞が先頭にくるのは少数派じゃないでしょうか(出展不明なWikipediaの語順によると18%だそうな)。よくあるオブジェクト指向言語のメソッド呼び出し「主語.動詞」のほうが日本語や英語の語順に近くて、それゆえに馴染み深く感じるのかなと思ったりします。VSO (動詞、主語、目的語)という語順の言語話者はLispに対してどういうふうに感じるんでしょう。比較したいところです。ハワイ語はVSOのようですが、ネイティブスピーカーはすごいマイノリティなんですね。
- koguro(2007/01/23 07:56:40 PST): 個人的には「主語.動詞」という構文は、場合によってはちょっと気持ち悪い感じがします。例えば、オブジェクト指向言語の場合、リストへの要素の追加はaList.add(1)などと書きますが、よく考えてみると「1をaListに追加する」のだから、本来主語は私かコンピュータ自身のはずでaListが主語の位置に来るのは変に思えます。こういったようにあるオブジェクトを操作するような場合、先頭にオブジェクトを配置するより、動詞にあたるメソッドを先に持ってきた方が、命令形が並んでいるように見えるので、より自然に感じるのですがどうでしょう。まあ、プログラミング言語を自然言語の観点から眺めても、どこかで無理が生ずるのでほどほどにしておくべきかなと思っていますが。
- Rui(2007/01/23 08:26:48 PST): aList.add(1)は、「aListが1を(自分自身に)追加する」というようなニュアンスで捉ませんか?
Lispだと式の先頭に操作が来る頭でっかちな式になりがちで、それが慣れないというのは、やっぱりあると思います。たとえば(for-each (lambda ...) lis)とLispで書くのを、たとえばRubyではlist.each { ... }と書きますよね。ほかの言語でも処理は後ろに書くことが多いはず。私はLispのfor-each式を(引数lisを先に書いたりせず)頭から書いていって平気ですが、それでもRuby的順番(処理を後に書く)のほうが書きやすいんじゃないかとたまに思います。これはちょっと話題が違っているかもしれないですが……。
プログラミング言語を自然言語でとらえるのは確かにすぐ無理が生じるんですが、ファーストインプレッション(あんなもの使えん/簡単そう)を決めるにはかなりの影響があるはず。SQLは不要なシンタックスシュガーを必須にしてまで英語っぽくしていますが、あれなんて、私にはよくわからないけど英語ネイティブスピーカーにはかなりフレンドリーに思えるんじゃないでしょうか。ほとんどのプログラミング言語が擬似英語っぽい文法で、その中でLispはあまり英語っぽくないように見えるという点は、Lispがハッカー専用言語みたいな本来はそうあるべきではない扱いになってる理由の1つのような気がします。(だからS式はダメだよねというわけではなくて、私はS式でいいんですが、世間的なとっつきやすさはいまいちなのはそういう理由もあるかな、と。)
- nekoie(2007/01/23 18:51:24 PST): (LispだけでなくC等でも)プログラミング言語を自然言語として考えた場合、プログラミング言語は一見、前置記法のように見えるけど実は前置記法ではなく、主語が実は「CPU」や「プログラマ」で、それは明らかに明示的なので省略されているだけにすぎない……という考え方はどうでしょう。(非CLOS系の、よくある)オブジェクト系の構文の場合は「(CPU/プログラマは命じる、)オブジェクト○○はmethod××しろ、引数△△で」という感じの解釈で。
(ちなみに、自分がはじめてS式をさわった時、例えばC等で「hoge(fuge, mage)」を考え、これが「(hoge fuge mage)」と等価なんだ……というような手順で理解していったので、「S式は別に分かりにくくはないし、むしろ命令構造が多段になった時でも構造関係が分かりやすくてより良い」と感じていたのを憶えてます。)
- koguro(2007/01/23 08:01:58 PST): condの括弧を減らしたら、条件判断を入れ替えたり、削除したりという操作がEmacsでちょっとやりにくくなるので、今のままがいいかもと思ってしまいます。
- Shiro(2007/01/23 11:35:15 PST): エディタについては、十分賢くなってプログラムの構造を 理解した編集が出来る、ということを想定しています。
- koguro(2007/01/24 05:40:00 PST): でも括弧を削減した場合、condの場合は述語と式をひとかたまりとして扱う、というような情報をエディタ側に与えなくてはならないと思いますが、condみたいなマクロを書くたびに、エディタにその情報を教えるのが面倒だなと考えてしまいます。
- nobsun(2007/01/23 14:50:34 PST): プログラム(算譜)がどのように表現されているかという問題ではないかと思います.Lispは関数プログラミングのスタイルで書かれていて,プログラムが木あるいはグラフとして書かれています.これは命令を実行順にならべて書くスタイルが刷り込まれたプログラマの違和感につながるのだと思います.語順が変とか...
私の場合,S式に対して不満なのは shiro さんも言及しているように (foo bar) の形式が関数適用である場合とグループ化のためのリストである場合を兼ねるのが嫌です.潔癖症の私(嘘...)としては,(foo [bar baz]) とか (let [[[x (...)] [y (...)]...] something]) とか (cond [[真理式 式] [真理式 式] ...])とかがいいんだけどねぇ.
[x y z ...]がリスト,(f x)が関数適用.というのはどう?とか言ってみるテスト
- Shiro(2007/01/23 21:03:59 PST): うーん、S式のパワーはプログラムがリストで表現されてるって とこにあるから、リストと関数適用を分けちゃうのはつらいなあ。特殊形式が 扱う部分構造に対して特別なリストを導入するだけなら、特殊形式の ハンドラががんばればいいだけなんで、あまり難しくないけど。 すごく極端な立場を取れば、リテラルリストを[x y z ...]にして、 (f x y ...)はリーダによって [syntax-node f x y ...] と読まれる、 とする手はある。これなら、特殊形式と関数適用はcarが'syntax-nodeで始まる リストになっていると考えるだけで良いのですっきりする。
- kiyoka(2007/01/24 04:33:44 PST): そういえば、今ではまったく気になりませんが、 Lispを始めた頃は (let ((a 1)(b 2)(c 3)) ... ) や condフォームを書く時、 『あれ?括弧をいくつ書いたらいいんだっけ』と迷うことが多かったです。 多分その時はインデントだけ見てれば良いということに気がつかなかったんだと思います。 自分の昔を思い起こすと、秀丸の様にLispをサポートしないエディタを使っている普通のプログラマに対して、 Lispを推薦することは絶望的な気がします。(Rubyと比較してPythonでもちょっと毛嫌いされる状況なので...) そういう構文上のハードルが高いのは否めません。
- Java を Eclipse 環境で書くと強い型と object.method() の順序と相まって非常に親切に補完をしてくれますよね。S式書くときはコンピューターに補完させることをあまり期待しないです。マクロはあくまで人間が簡潔に書けるようにするためのもので、開発環境がリッチな今風ではないなあ、と思うときがあります。
- Shiro(2007/02/04 00:27:29 PST): もうひとつ、とても大事なことを思いだした。 「Lisp設計者は、自分より言語ユーザの方が、そして現在のユーザより未来のユーザの方が、 賢いと考えている」。だから現在までの自分の知見で構文をfixするよりも、 材料を全部揃えておいてより賢いユーザ (将来の自分を含む) により良い構文を 作ってもらおうと考える。 問題は、このスタンスだと永遠に構文がfixしないことである。
- grafi(2013/03/15 01:59:46 PST) すごく今更ですが、ちょうど今お遊びのScheme処理系をSchemeで実装していて多値の扱いなど考えていたら気になったのでコメントしてみます。
applyがしっくりこないという話ですが、quasiquoteで用いる @ をsplicingのための一般的な記法とすれば良いのではないかと思いました。 (define (bar x @opts) (foo 0 x @opts)) みたいに書ければ良いんじゃないかと思います。ついでに (define (bar @list opt) (foo 0 @list opt)) と書けてもいいかも。
そう考えた根拠みたいな話をします。何故 (foo 0 x . opts) のような書き方が上手く行かないかというと、そもそも . はS式レベルでの意味を操作する記法であり、一方関数呼び出しの際の引数は、任意の一つのS式を取るのではなく、0個以上のS式の単なるリスト(あるいは実装の上では単なるスタック)だからじゃないかと思います。関数呼び出しを構文上S式によるリストで表現するからといって、混同して . を使っていると、仮引数部ではたまたま問題が出なかったものの、呼び出し側ではおかしなことになったということでしょうか。
また、多値が渡されることを許される継続をもっと広げた上で、 @ をリストから多値に変換する省略記法だと考えれば、かなり意味の上でもすっきりしそうだと思いました。 quasiquoteも `(a b ,@(map (lambda (x) (* x x)) '(1 2 3)) ,(+ 13 29)) #=> (list 'a 'b @(map (lambda (x) (* x x)) '(1 2 3)) (+ 13 29)) と展開されると考えると、統一できるのではないかなと。逆に、仮引数部での @ は、多値からリストへの変換ですね。どこで区切れば良いかを決定出来なくなるので一箇所しか使えなくはなりますが、十分対称的だと思います。
- Shiro(2013/03/15 09:53:34 UTC): ドットリストを仮引数に使うことの欠陥はご指摘の通りです。私はこれはSchemeの設計ミスだと思っています。
@のアイディアについては私も永らく頭の隅で考えているのですが、トレードオフはマクロとの相互作用だと思います。多分、最初から@が存在するという前提の言語なら、「そういうもの」と考えてマクロを書けば良いので下の1の選択肢で良いと思うんですが、Schemeとして既にあるマクロもちゃんと使えることを目指そうとすると互換性の問題にあたると思います。- 意味的な一貫性を考えるなら、@の処理はマクロが全て展開された後で、関数呼び出しであることがはっきりしている箇所の引数リストに@があればapply扱いにする。しかしこの場合、関数の引数を操作するようなマクロは@の存在を考えてかかれなければなりません (例えば「引数がx個の場合はこういう展開」というよな処理を行うところ全てに、「引数リストに@が含まれていないこと」のチェックを入れる必要がある。また、S式を再帰的に降りてゆく処理も全て@の存在を気にする必要がある)
- @をリーダレベルの構文糖衣として、呼出側の引数に@があったら問答無用にapplyを入れてしまう。これだとマクロは@の存在を気にする必要がないが、(foo x @y) のfooがマクロだった場合に困る。
- ちょっとした思いつきなのですけれでも,新しくリテラルとしてのboxを定義すれば(foo x . opts)の問題が解決できそうな気がします.boxは要素数1のコレクション型で&fooと表記するとします.ここで,boxは
'&foo => &foo &0 => 0 &(+ 1 2) => 3 &(list 1 2) => (1 2)
のように評価されるとします.つまり,boxを評価した結果はその中身の評価結果となります.すると,(foo x . &(bar y))は(foo x bar y)ではなく,(apply foo x (bar y))のようになるはずです.これはリードマクロではないので(foo . ,(bar x)) == (foo unquote (bar x))のようになる問題も発生しません.マクロが関数の引数を操作する場合も関数適用の構文がimproper listであるということを考慮するだけで正しく操作できると思います.- Shiro(2017/07/17 00:19:07 UTC): boxの動作はわかりますが、それだけではだめで追加の評価規則が必要ですよね。(foo x . y)の形は現行のルールでは評価できないので。
そこで例えば、Eval[(foo x . y)] => Apply[Eval[foo], List*[Eval[x], Eval[y]]]
という規則を足してみます(環境は省略)。これは現行のEval[(foo x y)] => Apply[Eval[foo], List[Eval[x], Eval[y]]]
の自然な拡張で、yがboxの場合におっしゃる通り動作します。
しかしこの規則を入れると、yがリストの場合に曖昧性を生じてしまうので、「yがリストでない場合のみこの規則を使う」というような条件が入ってくるところが面倒ではあります。まあquasiquoteの展開マクロ程度の面倒さで済むとは思いますが。
- Shiro(2017/07/17 00:19:07 UTC): boxの動作はわかりますが、それだけではだめで追加の評価規則が必要ですよね。(foo x . y)の形は現行のルールでは評価できないので。