関数型の基本:型、値、バインディング
ごく簡単に説明すると、関数型プログラミングは、値の抽象性を高め、値を操作する方法を改善します。そのために、通常の標準データ型やクラスを超える、次のようなデータ抽象化メカニズムが用意されています。
- 弁別子付き共用体(閉じたデータ表現を作成できる)
- タプルとレコード(データの集合をより簡単に作成できる)
- 関数型(シグネチャの定義と強制を可能にする)
- ポリモーフィックな抽象データ型(ジェネリックな構造化データを表現できる)
- シーケンス(オンデマンドで操作できるコレクション - 例えば、乱数の無限ストリームを作成し、好きなだけ値を読み取ることができる)
これらの型は、次のようにわずかなコードで定義できます。
// 名前と年齢を持つ個人。レコードとして定義する。
type Person = { name: string; age: int; }
// 名前とその妥当性を表すペア/タプル。
type NameValid = string * bool
// ジェネリックな述語型(値をブール値にマップする関数)
type 'a predicate = 'a -> bool
// 以下の表現は等価
type IntList = int list
type IntList' = List
// すべての自然偶数は(ほぼ)無限のシーケンスとして表現される。
let evenNumbers = seq { 0 .. 2 .. Int32.max_int }
最後の行は、値の定義、つまり値と名前のバインディングです。ここではトップレベルのバインディング(モジュールレベルのバインディング)で表現していますが、letバインディングはプログラムのどこでも使用できます。任意のレベルにネストされたローカル関数と定義を簡単に作成できます。
関数型プログラミングでは、関数はファーストクラスの値としてデータ値とまったく同じ扱いとなるため、関数をデータ構造に埋め込んで、他の関数に渡したり他の関数から受け取ることができます。このような関数を高階関数と呼びます。この機能によって、クラス型は複数の関数フィールドを持つレコードとなるため、関数型プログラミング言語はオブジェクト指向機能を容易に組み込むことができます。
不変性、クロージャ、型推論とは
関数型プログラミングの根本概念は「不変性」です。つまり、作成した値が変わらないことです。オブジェクト指向言語や命令型言語では変数を使って値を変更するのが当たり前ですから、それとは好対照です。関数型プログラミングでは、値を変更するのではなく、変更された新しいコピーを作成します。具体的には、通常の、パラメータが状況に応じて変化する命令型void関数を、パラメータのコピーを変更して返す関数に置き換えます。新しいコピーを作成するのは不経済と感じるかもしれませんが、そうではありません。コンパイラによって巧妙な戦略が適用され、コピーおよび変更の一連の操作は大幅に最適化されます。条件によっては、命令型コンパイラをも超える性能を発揮できます。
そうなると、変数を使う必要はあるのでしょうか。厳密に言えば、使う必要はありません(命令型コードを書こうとする場合は話が別ですが)。次のようなコードがあるとしましょう。
let printSumDoubled x y = printf “The result is = %d\n” ((x+y)*2)
let _ =
let a, b = 172, 201
printSumDoubled a b
let a, b = 215, 347
printSumDoubled a b
1行目は、2つの引数の合計を2倍して、その結果をプリントする関数の定義です。データ型を記述しなくてよいことに注目してください。コンパイラがパラメータの使い方を見て、型を判断します。この例では、xをyに足すのですから、xとyはどちらも必然的に整数型です(他の型を宣言しない限り、これが算術演算子のデフォルトの型です)。また、正規パラメータをかっこで囲む必要もなければ、パラメータをカンマで区切る必要もないことも注目に値します。かっこやカンマを使うこともできますが、それは関数が2つの引数ではなく1つのタプル引数を受け取るという意味になるため、関数呼び出しをprintSumDoubled(a, b)のように変える必要があります。心配しなくても、関数に渡す引数が多すぎる場合は、コンパイラがそれを指摘してくれます。しかし、クロージャ(自らの状態情報を内部に持つ関数)を作成するときには、次のように渡す引数が少ない方が良いでしょう。
let printSumWithFiveDoubled = printSumDoubled 5
このprintSumWithFiveDoubledは、1つの整数型引数を受け取り、その値に5を足してから2倍した結果をプリントする関数です。基本的にはprintSumDoubled関数をインスタンス化したものですが、その事実は次のように定義した方が理解しやすくなるでしょう。
let printSumWithFiveDoubled y = printSumDoubled 5 y
上記のコードブロックのletバインディングは、1行ごとに1つの代入を記述してこれらを連結しています。しかし、水面下ではずっと複雑なことが行われます。ブロックの1行目で、値(172, 201)は左側の(a, b)のパターンにマッチします。単純明快なマッチングなので、これらの値はそれぞれaとbにバインディングされます。当然ながら、任意の複雑な式に対してパターンマッチを指定し、多数のバインディングを一度に作成することもできます。または、まったくバインディングを作成しないことも可能です。例えば、_に対するトップレベルのバインディングは、計算式の値をそのまま提出することを意味します。戻り値がないらしい関数(C#のvoidに相当するもの)は、戻り値の型がunitになります。実際には、この型は()として表現される1つの"値なし"を値として持ちます。上記の例では、この空の値は無視されます。
ここで重要なのは、バインディングは通常の意味での変数とは異なるということです。バインディングは特定の値を示す名前でしかなく、別の値を代入すると前の値はその陰に隠れてしまいます。実際、上記のコードは次のように記述しても同じです。
let printSumDoubled x y = printf “The result is = %d\n” ((x+y)*2)
let _ =
let a, b = 172, 201
printSumDoubled a b
let newA, newB = 215, 347
printSumDoubled newA newB
