// Java
void print123() {
System.out.println("1");
System.out.println("2");
System.out.println("3");
}
お手軽入出力
従来、純粋関数型言語においては、入出力は不可能とまでは言わないまでも難しく扱いにくいものだと考えられてきました。
簡単な例として、コンソールに 1、2、3 を出力するためのメソッド (Java および Groovy) および関数 (Frege) を挙げ、比較してみましょう。
この例は単純化しすぎているように見えますが、実は思い切って普段の常識を疑ってみることで面白いことが学べます。
コード比較:Java 対 Groovy 対 Frege
今回、比較する対象は以下のコードです。
// Groovy
def print123() {
println "1"
println "2"
println "3"
}
Groovy 版は書き方がいくつも考えられます。Java と全く同じ書き方をすることも可能で、それもまた正しい Groovy プログラムです。今回はもっとコンパクトなバージョンを採用しました。
-- Frege
print123 = do
println "1"
println "2"
println "3"
驚くべきことに、3 種類の中で 最も短いのは Frege で、4 文字分ですが Groovy 版よりも若干短くなっています! さらに、記号類は Frege 版が一番少ないことがわかります。
上の例には、明らかに同じコードの繰り返しが含まれています。Groovy でも Frege でも 3 行分をまとめて 1 行にすることは簡単にできるのですが、この件は後の記事で扱います。
しかし、ここで驚嘆すべきポイントがあります。Frege 版は最も短いというだけではなく、何が起こるかについて最も明示的なのです!
最も明示的なのは?
各言語の型シグネチャからわかることをまとめてみましょう。
Java |
このメソッドは |
Groovy |
Groovy は Java にとてもよく似た特徴を持っていますが、例外に関してはもっと誠実です。例外は必ず記述されているなどと騙ったりしません。ここで使用している戻り値の型 |
Frege |
今回のコードは型について何も語っていませんが、型が存在しないわけではありません。型は 推論 されており、REPL で |
根本的な違い
根本的な違いとして、Java や Groovy のような命令型言語では、表面上、3 回の println
文の間に関連性がないことが挙げられます。文を並べ替えたり、完全に無関係な計算を間に挟むことが可能で、型シグネチャは変化しません。
Frege では状況は全く異なります。Frege には文が存在せず、あるのは式だけです。
println "2"
と println "3"
は、単に 2 行が順番に並んでいるというわけではなく、極めて強い関連を持っています。この 2 行は両方とも式であり、IO 型が指定する方法で両者を バインド する関数の引数になっています。バインド された結果の戻り値の型は二つ目の引数と同じで、ここでは IO ()
です。
-- pseudocode
bind (println "2") (println "3")
そして当然、println "1"
についても同じことが言えます。
-- pseudocode
bind (println "1") (bind (println "2") (println "3"))
Note
|
バインド は、関数名としては IO 型クラスには現れません。演算子 >>= の形でのみ定義され、この演算子を「バインド」と読みます。すなわちここで挙げた疑似コード bind a b は実際のコードでは a >>= b となります。
|
命令型プログラムのように見えますが、関数全体で 1 つの式になっているため、型推論は非常に簡単です。
以上のように、様々なものをキーワード do
によって 1 つの式に押しつぶすことができ、このキーワードは結果となる式の バインド 関数と呼ばれています。今回の式は IO
型であるため、IO.bind
が使用されます。
例えば「静かなる記号たち」ですでに見たとおり、関数型プログラミングの意味論ではプログラムは最後から遡って読むことになります。これは「do」記法でも同様です。最終的な結果の型を確認するために、今回は右から左ではなく、下から上に向かって読みます。
Note
|
個人的な経験から
自分の場合、この仕組みがどうやって動いているのか、特に do が使用する bind を一体どのように判断するのか、理解するのにしばらく時間がかかりました。このあたりの説明は世の中に山ほどありますが、自分が知りたい点だけは見つけられませんでした。もしかすると他の人にとっては自明すぎるのかもしれません。
|
ちなみに、bind を中置演算子のように使ってみると面白い類似が見て取れます。
-- pseudocode a `bind` b `bind` c
命令型言語のスタイルで以下のように書いた場合とよく似ています。
a ; b ; c
これが、bind が冗談半分に プログラマブル・セミコロン などと呼ばれる理由です。bind は二つの関数を与えられたコンテクストにおいてどのように合成するか、また後で見るように、最初の関数の結果をどのように次の関数の引数として バインド するかを規定します。
まとめ
-
IO 処理は、Frege のような純粋関数型言語であっても、実に簡単に書くことができる
-
純粋関数型言語では、IO が存在しないわけではなく、IO について厳密に明示しなくてはならない
-
関数型のコードは、その本質を損なうことなく命令型のコードであるかのように見せることができる
-
副作用が存在するとき、do 記法が役に立つ