TL;DR
- 関数を全域関数として設計すると、すべてのエフェクトを型シグネチャで文書化できる。
- 全域関数とは、取りうる各入力にそれぞれ 1 つの出力が対応する関数である。例外を投げるとそうならないので、入力を制限するか
Option型を使う。
内容
Understanding Functions の一節である。
数学の関数は、取りうる各入力にそれぞれ 1 つの出力が対応するものである。これを全域関数と呼ぶ。関数型プログラミングでも同じように関数を設計するのがよい。そうすると、すべてのエフェクトを型シグネチャで文書化できる。
例外を投げる関数は全域関数ではない
12 を入力値で割った結果を返す関数を考える。
let twelveDivideBy n =
match n with
| 6 -> 2
| 5 -> 2
| 4 -> 3
| 3 -> 4
| 2 -> 6
| 1 -> 12
| 0 -> ???0 が渡されたときに例外を投げることにすると、シグネチャは twelveDivideBy : int -> int になる。
let twelveDivideBy n =
match n with
| 6 -> 2
...
| 0 -> failwith "Can't divide by zero"このシグネチャを見ると、int を渡せば必ず int が返るように思える。しかし実際は例外が起きる可能性があるので、int が返るとは限らない。つまり全域関数ではない。
全域関数にする方法
シグネチャが明示的になるように、全域関数にしたい。方法は 2 つある。
1 つ目は、入力を制限して不正な値を排除する方法である。0 は入力になりえないので、処理する必要がない(ビジネスルールを型システムで表現する)。値の作成にはスマートコンストラクタを使う。
type NonZeroInteger =
// ゼロではない整数になるように定義
private NonZeroInteger of int
let twelveDivideBy n =
match n with
| 6 -> 2
...
// 0 は入力になり得ないので処理する必要がない2 つ目は、出力を広げる方法である。Option 型を使い、シグネチャを twelveDivideBy : int -> int option にする。
let twelveDivideBy n =
match n with
| 6 -> Some 2
...
| 0 -> None