SMLを書くLisperの悩み
SMLばっかり書いてたら「お前Lisperじゃないだろ」って怒られたとかそういう話ではなく。
ML系の言語は関数は1つの引数しかとれません。じゃあ複数の値を受け取りたい時はどうするかというと 1. 値を組(タプル)にして受け取る 2. 関数を返す関数((を返す関数)*)にして1つづつ受け取る の2種類の方法があります。 それに纏わる話。
それぞれの記法を例示すると
-
値を組(タプル)にして受け取る
fun add (x, y) = x + y add (1, 2)
あるいは、手続型言語に似せて
add(1, 2)
とするスタイルもあります
-
関数を返す関数((を返す関数)*)に(カリー化)して1つづつ受け取る
fun add x y = x + y add x y (*= ((add x) y) *)
となります。勿論、2.のように何度も関数を呼び出すよりは1.のように一度で全ての値を渡してしまった方が速い筈です。なのでプリミティブっぽい関数はタプル式にした方が良さそうです。
また、無名関数を定義する時にSMLではfun
のように自動でカリー化してくれる構文がないので複数の引数を受け付ける関数を引数にとる高階関数を定義するときはタプル式の方が都合が良いでしょう。
例えば二項演算子はタプル式で定義しなければなりませんし、List.foldl
のシグネチャも
('a * 'b -> 'b) -> 'b -> 'a list -> 'b
です。
勿論、積極的に最適化を行なうコンパイラではuncurry optimizationや、closure eliminationでカリー化によるオーバーヘッドはなくなります。 むしろ、カリー化した方が部分適用が出来るので利便性は上がります。となると後は無名関数の問題ですが、OCamlやHaskellなど無名関数にもカリー化した定義が出来る構文のある言語だとそれも問題なく、タプル式の引数の渡し方はしないようです。
SMLは流石に’Standard’なので最適化を仮定したり余計な構文を突っ込んだりはしづらいのでしょう。なのでこの問題はSML特有のようです。
ここまで、なぜタイトルがML系言語ではなくSML限定なのかの前置き。
こういう関数呼び出しがあるとします。sub: string * int -> char
は文字列の0番目の文字を取り出します。
sub(str, 0)
これにChar.isAlpha : char -> bool
を適用します。
Char.isAlpha sub(str, 0)
これ、コンパイルエラーになります。みなさん何でか分かりますか?
sub(str, 0)
はあくまでsub
に(str, 0)
というタプルを渡している文なのでコンパイラはこう解釈するのです。
(Char.isAlpha sub) (str, 0)
毎回このようなエラーを出すのが面倒なので関数の呼び出しには全て手続き型言語のように括弧をつけることを考え始めます。
Char.isAlpha(sub(str, 0))
しかしそうは問屋が卸さないのがカリー化された関数。String.isPrefix: string -> string -> bool
に次のような呼び出しをすると勿論怒られます。
String.isPrefix("/usr/local" path)
必ず
String.isPrefix "/usr/local" path
としないといけません。これまた関数呼び出しがネストすると面倒で、
app (fn s => print (s ^ "\n")) (List.filter (String.isPrefix "/usr/local") paths)
のように毎回括弧が付き纏います。ここまでくるとS式みたく
(app (fn s => print (s ^ "\n")) (List.filter (String.isPrefix "/usr/local") paths))
と書きたくなってくるのがLisperの心情。これなら慣れないデータコンストラクタや関数呼び出しや中置演算子の優先順位問題も解決!やったね!!と思ったのですがやっぱりタプル式の関数呼出が行く手を阻むのでした。
(sub(str, i))
とか訳分からなすぎる。括弧多すぎる。
ということでどっちに揃えたら良いのかさえ分かってないのにどっちにも揃えられてないSMLに対して悩みを抱えるLisperの悩みでした。
因みにSML-LintはChar.isAlpha()
のような書き方は無駄な括弧がついてると怒ってきます。