関数型プログラミング言語の構文は「すべてが式」ではない

関数型プログラミング言語の構文は「すべてが式」であるという標語がしばしばみられるが、これはいいすぎ。

結論

本稿の趣旨自体が重箱のすみをつつくようなものなので、結論から書く。

  • 言葉の曖昧性をわきにおいても、「すべてが式」という表現はパターンや宣言を無視していて、事実でない
  • 「C系の言語では伝統的に文である構文が、関数型言語ではたいてい式である」ということをいいたいのだろうし、それは事実にあっている

例に使う言語

普段なら例として (筆者が慣れている) F# をあげるところだが、F# は出自的に C# の影響を非常に強く受けているので「伝統的な関数型言語」の例としては受け入れられないかもしれない。 本稿では「伝統的な関数型言語」と思われるOCamlとHaskellを例にとる。 (そもそも「関数型言語」が何かも曖昧だけど。)

トップレベル構文は式でない

「すべてが式」を文字通り解釈すると、ソースファイルに書かれたコード全体が1個の式として解釈可能である、という主張が含意されるようにみえる。 少なくともこの2つの言語において、その主張は事実でない。

OCamlはソースファイルには、実装ファイルとインターフェイスファイルの2種類がある。 実装ファイルの全体はモジュールアイテムの0個以上の並びとして定められている。 モジュールアイテムは定義や式の並びからなるが、定義には型定義のような式ではないものが含まれている。 (型定義は type キーワードから始まるが、式は type キーワードから始まらない。) したがって、ソースファイル全体を1個の式と解釈するのは厳しい。

unit-implementation ::= [ module-items ]

module-items ::= {;;} ( definition ∣ expr ) { {;;} ( definition ∣ ;; expr) } {;;}

definition ::= … ∣ type-definition ∣ …

type-definition ::= type [nonrec] typedef { and typedef }

インターフェイスファイルのほうは仕様の並びとして定められている。 仕様にも型定義を書けるので、式とは解釈できない。

unit-interface ::= { specification [;;] }

specification ::= … ∣ type-definition ∣ …

参考: 7.12 Compilation units

同様に、Haskellのトップレベルも式ではない。 型や関数の定義のような、いわゆる「宣言」の並びになっている。

参考: 10 Syntax Reference (Haskell 2010のマニュアルの一部)

式でない構文

「すべてが式」でないなら、式でない構文は何か。 もちろん言語によるが、典型的には「パターン」「型」「定義・宣言」などがある。

パターンは文字通りパターンマッチで使われるやつ。 構文的に式にそっくりだが、式ではない。 例えばif式は式だがパターンではない。 反対に (Some 0) as opt のようなパターンは明らかに式でない。

上記で触れたように、型や関数を定義するのに使う「定義」(または「宣言」)も、たいていの言語において式でないと思う。 (余談: 言語によって定義と呼んでたり宣言と呼んでたりする。 実際OCamlは定義と呼んでて、Haskellは宣言と呼んでる。)

「型」の構文もある。 (念のためにいうと、型定義ではない。式の型を明記する部分の構文のこと。) 例えば a -> b は型aの値を受け取って型bの値を返す関数の型。 (型の構文を式の構文の厳密なサブセットにことは可能。)

文っぽいけど式な構文

「すべてが式」といわれるゆえんは、C系の言語において文として提供されがちな構文が式になっていることが多いからだと思う。 具体的にはlet式と条件分岐とループ。

ifやmatchが式なのはよく知られていると思う。

ループも式だが、OCamlのループは値が () なのであまり式っぽくない。 文というくくりがないから式、という感じがある。 (Rustの loop 式は () 以外の値を持てて、かなり式っぽい。)

let式はローカル変数を導入して、それを使って何か計算する、という式である。

    let x = 3 in x * x;;  (* => 9 *)

並べて書くと文にみえるが、単に長くて深い式が複数行に書かれているだけ (inの後ろが末尾まで広がっている)。

    let x = 3 in
    let y = x * 4 in
    y * y;;         (* => 144 *)

余談: ある構文要素が式であるか、文や宣言であるかの判断は、それをカッコで囲めるか考えると分かりやすいかもしれない。 たいていの言語では式をカッコで囲めるが、文や宣言は囲めないため。

    (let x = 3 in x);;   (* OK *)
    (type a = A);;       (* NG *)

関数の本体は式

文と式の境界がないおかげで、1個の式に多くの記述をつめこめる。 構文的に関数の本体は1個の式と定義されている。 (OCamlではexpr、Haskellではexp。)

そういうわけで、関数の外側や細かい部分に目を瞑れば「すべてが式」といえた……?

次回予告?

(ほんとうは「継続を考えると文がない言語でもジャンプ命令は自然に解釈できる」という話につなげたかったけど、到達しなかったので、別の記事を書きたい。)

関連記事