気づきにくいかもしれませんが、F#には実は2つの構文があります。1つは普通の(値)式用で、もう1つは型定義用です。例を見てみましょう。
[1;2;3] // 普通の式
int list // 型式
Some 1 // 普通の式
int option // 型式
(1,"a") // 普通の式
int * string // 型式
型式には、普通の式とは違う特別な構文があります。対話型セッションを使うと、各式の型がその評価結果と一緒に表示されるので、すでに多くの例を目にしているはずです。
ご存知の通り、F#は型推論で型を推測するので、特に関数の場合、コードで明らかに型を示す必要はあまりありません。でも、F#を上手に使いこなすには、型構文を理解する必要があります。これで、自分で型を作ったり、型エラーを直したり、関数シグネチャを理解したりできるようになります。この記事では、関数シグネチャでの使い方に焦点を当てます。
以下に、型構文を使った関数シグネチャの例をいくつか示します。
// 式構文 // 型構文
let add1 x = x + 1 // int -> int
let add x y = x + y // int -> int -> int
let print x = printf "%A" x // 'a -> unit
System.Console.ReadLine // unit -> string
List.sum // 'a list -> 'a
List.filter // ('a -> bool) -> 'a list -> 'a list
List.map // ('a -> 'b) -> 'a list -> 'b list
シグネチャから関数を理解する
関数のシグネチャを見るだけで、その関数が何をするのかある程度推測できることがあります。いくつかの例を見て、順番に分析してみましょう。
// 関数シグネチャ 1
int -> int -> int
この関数は2つの int
引数を受け取り、別の int
を返します。おそらく、加算、減算、乗算、べき乗などの数学的な関数だと考えられます。
// 関数シグネチャ 2
int -> unit
この関数は int
を受け取り、 unit
を返します。つまり、関数が副作用として何か重要なことをしているということです。役に立つ戻り値がないので、副作用はおそらくログを記録したり、ファイルやデータベースに書き込んだりするなど、IOに関係する処理でしょう。
// 関数シグネチャ 3
unit -> string
この関数は何も入力を受け取らず、 string
を返します。つまり、この関数は文字列を無から生み出しているのです!明らかな入力がないので、おそらく(ファイルなどからの)読み取りや(ランダムな文字列などの)生成に関係していると考えられます。
// 関数シグネチャ 4
int -> (unit -> string)
この関数は int
の入力を受け取り、呼び出すと文字列を返す関数を返します。この場合も、読み取りや生成に関係している可能性が高いです。入力は、返される関数を何らかの形で初期化していると思われます。たとえば、入力がファイルハンドルで、返される関数が readline()
のようなものかもしれません。あるいは、入力がランダムな文字列生成器のシードかもしれません。正確なところはわかりませんが、ある程度推測はできます。
// 関数シグネチャ 5
'a list -> 'a
この関数は何らかの型のリストを受け取り、その型の要素を1つだけ返します。つまり、この関数はリストの要素をまとめたり、選んだりしていると考えられます。このシグネチャを持つ関数の例として、 List.sum
、 List.max
、 List.head
などがあります。
// 関数シグネチャ 6
('a -> bool) -> 'a list -> 'a list
この関数は2つの引数を取ります。1つ目は何かをboolに変換する関数(述語)で、2つ目はリストです。戻り値は同じ型のリストです。述語は値が何らかの基準を満たすかどうかを判断するのに使います。そのため、この関数はリストから述語が真となる要素を選び、元のリストの一部を返しているように見えます。このシグネチャを持つ典型的な関数は List.filter
です。
// 関数シグネチャ 7
('a -> 'b) -> 'a list -> 'b list
この関数は2つの引数を取ります。1つ目は型 'a
を型 'b
に変換する関数で、2つ目は 'a
のリストです。戻り値は異なる型 'b
のリストです。おそらく、この関数はリスト内の各 'a
を取り、1つ目の引数として渡された関数を使って 'b
に変換し、新しい 'b
のリストを返すのでしょう。実際、このシグネチャを持つ代表的な関数は List.map
です。
ライブラリの関数を見つけるのに関数シグネチャを使う
関数シグネチャは、ライブラリの関数を探すときの重要な手がかりになります。F#ライブラリには何百もの関数があり、最初は圧倒されるかもしれません。オブジェクト指向言語と違って、オブジェクトに対して単純に「ドット」を使って適切なメソッドをすべて見つけることはできません。でも、探している関数のシグネチャがわかっていれば、候補を素早く絞り込めることが多いです。
たとえば、2つのリストがあって、それらを1つにまとめる関数を探しているとします。この関数のシグネチャはどうなるでしょうか?2つのリストを引数に取り、同じ型の3つ目のリストを返すので、シグネチャは次のようになります。
'a list -> 'a list -> 'a list
次に、F# Listモジュールのドキュメントを開いて、関数のリストを下に見ていき、一致するものを探します。実は、このシグネチャを持つ関数は1つしかありません。
append : 'T list -> 'T list -> 'T list
これがまさに私たちが探していた関数です!
関数シグネチャ用の独自の型を定義する
時には、望む関数シグネチャに合わせて独自の型を作りたいことがあるかもしれません。これは type
キーワードを使ってできます。シグネチャの書き方と同じように型を定義できます。
type Adder = int -> int
type AdderGenerator = int -> Adder
これらの型を使って、関数の値や引数に制限を設けることができます。
たとえば、以下の2つ目の定義は型制限のために失敗します。型制限を外せば(3つ目の定義のように)問題なく動きます。
let a:AdderGenerator = fun x -> (fun y -> x + y)
let b:AdderGenerator = fun (x:float) -> (fun y -> x + y)
let c = fun (x:float) -> (fun y -> x + y)
関数シグネチャの理解度をテストする
関数シグネチャをどのくらい理解できているでしょうか?以下のそれぞれのシグネチャに合う簡単な関数を作れるか試してみてください。明示的な型注釈は使わないようにしましょう!
val testA = int -> int
val testB = int -> int -> int
val testC = int -> (int -> int)
val testD = (int -> int) -> int
val testE = int -> int -> int -> int
val testF = (int -> int) -> (int -> int)
val testG = int -> (int -> int) -> int
val testH = (int -> int -> int) -> int