Lisp:S式の理由

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内:

外部リンク:

LispUser.netさんのところに素晴らしい エントリが。 新しいcond構文やlet構文をマクロで実装する様子が動画で見られます。 EmacsのS式編集機能や補完もばりばり使ってるので、Lisperのコード書きの様子が 知りたい人も必見。

議論、コメント

More ...