関数 (F#)
関数は、あらゆるプログラミング言語においてプログラムの実行の基本となる単位です。 他の言語の場合と同様に、F# の関数にもそれぞれ名前と本体があり、パラメーターや引数を受け取ることができます。 F# ではさらに、関数型プログラミング構成要素もサポートしています。たとえば、関数を値として処理したり、名前のない関数を式で使用したりできます。また、関数の合成による新しい関数の作成、カリー化関数、関数の引数の部分適用による関数の暗黙の定義などがサポートされます。
関数を定義するには let キーワードを使用します。再帰関数の場合は、let rec というキーワードの組み合わせを使用します。
// Non-recursive function definition.
let [inline] function-name parameter-list [ : return-type ] = function-body
// Recursive function definition.
let rec function-name parameter-list = recursive-function-body
解説
function-name は、関数を表す識別子です。 parameter-list は、スペースで区切られた一連のパラメーターで構成されます。 「パラメーター」で説明するように、各パラメーターの明示的な型を指定できます。 特定の引数型を指定しない場合は、型が関数本体から推測されます。 function-body は式で構成されます。 関数本体を構成する式は、通常、複数の式から成る複合式で、その最後の式が戻り値になります。 return-type では、コロンの後に戻り値の型を指定します。これは省略可能です。 戻り値の型を明示的に指定しない場合は、最後の式から特定されます。
単純な関数定義の例を以下に示します。
let f x = x + 1
この例の関数の名前は f、引数は x、引数の型は int、関数本体は x + 1、戻り値の型は int です。
inline 指定子は、コンパイラに対するヒントであり、関数のサイズが小さいためにコードを呼び出し元の本体に統合できることを表します。
スコープ
モジュール スコープ以外のレベルのスコープでは、値や関数名を再利用してもエラーになりません。 名前を再利用すると、後から宣言した名前が前に宣言した名前をシャドウします。 ただし、モジュールの最上位のスコープでは名前が一意である必要があります。 たとえば次のコードは、モジュール スコープではエラーになりますが、関数内ではエラーになりません。
let list1 = [ 1; 2; 3]
// Error: duplicate definition.
let list1 = []
let function1 =
let list1 = [1; 2; 3]
let list1 = []
list1
これに対して、次のコードは、どのレベルのスコープでも許容されます。
let list1 = [ 1; 2; 3]
let sumPlus x =
// OK: inner list1 hides the outer list1.
let list1 = [1; 5; 10]
x + List.sum list1
パラメーター
パラメーターの名前は関数名の後に指定します。 次の例のように、パラメーターの型を指定できます。
let f (x : int) = x + 1
型を指定する場合は、パラメーターの名前の後にコロンで区切って指定します。 パラメーターの型を省力した場合は、コンパイラによって推論されます。 たとえば次の関数定義では、1 の型が int であるため、引数 x の型は int と推論されます。
let f x = x + 1
ただし、コンパイラは、可能な限り、関数を汎用的にしようとします。 たとえば次のようなコードがあるとします。
let f x = (x, x)
この関数は、任意の型の 1 つの引数からタプルを作成します。 この関数では型が指定されていないため、任意の型の引数を使用できます。 詳細については、「自動ジェネリック化 (F#)」を参照してください。
関数本体
関数本体には、ローカル変数と関数の定義を含めることができます。 それらの変数と関数のスコープは、現在の関数の本体に限られます。 軽量構文オプションを有効にしている場合は、次のように、定義が関数本体に含まれていることをインデントによって示す必要があります。
let cylinderVolume radius length =
// Define a local value pi.
let pi = 3.14159
length * pi * radius * radius
詳細については、「コードのフォーマットに関するガイドライン (F#)」および「冗語構文 (F#)」を参照してください。
戻り値
コンパイラは、関数本体の最後の式を使用して戻り値とその型を特定します。 最後の式の型が前の式から推論される場合もあります。 前のセクションの関数 cylinderVolume の pi の型は、リテラル 3.14159 の型から float と特定されます。 さらに、pi の型を使用して、式 h * pi * r * r の型が float と特定されます。 このため、関数全体の戻り値の型が float になります。
戻り値を明示的に指定するには、次のようなコードを使用します。
let cylinderVolume radius length : float =
// Define a local value pi.
let pi = 3.14159
length * pi * radius * radius
この場合、関数全体に float が適用されます。パラメーターの型にもこの型を適用するには、次のコードを使用します。
let cylinderVolume (radius : float) (length : float) : float
関数の呼び出し
関数を呼び出すには、関数名の後にスペースを入れ、その後に各引数を指定します。各引数はスペースで区切ります。 たとえば、関数 cylinderVolume を呼び出して結果を値 vol に割り当てるには、次のコードを使用します。
let vol = cylinderVolume 2.0 3.0
引数の部分適用
指定されている数より少ない数の引数を渡すと、残りの引数を使用する新しい関数が作成されます。 これは、カリー化と呼ばれる引数の処理方法で、F# のような関数型プログラミング言語の特徴の 1 つです。 たとえば、半径がそれぞれ 2.0 と 3.0 の 2 つのパイプがあるとします。 この場合、次のようにして、パイプの体積を特定する関数を作成することができます。
let smallPipeRadius = 2.0
let bigPipeRadius = 3.0
// These define functions that take the length as a remaining
// argument:
let smallPipeVolume = cylinderVolume smallPipeRadius
let bigPipeVolume = cylinderVolume bigPipeRadius
その後、必要に応じて、2 つのサイズのパイプのさまざまな長さを追加の引数として指定します。
let length1 = 30.0
let length2 = 40.0
let smallPipeVol1 = smallPipeVolume length1
let smallPipeVol2 = smallPipeVolume length2
let bigPipeVol1 = bigPipeVolume length1
let bigPipeVol2 = bigPipeVolume length2
再帰関数
再帰関数とは、自身を呼び出す関数です。 再帰関数を使用するには、let キーワードの後に rec キーワードを指定する必要があります。 関数の本体から再帰関数を呼び出す方法は、他の関数呼び出しの場合と変わりません。 次の再帰関数は、n 番目のフィボナッチ数を計算します。 フィボナッチ数列は、古代から知られている数列で、数例の各数値が、前の 2 つの数値の和になります。
let rec fib n = if n < 2 then 1 else fib (n - 1) + fib (n - 2)
再帰関数は、特殊な技巧 (アキュムレータや継続の使用など) を意識して慎重に使用しないと、スタック オーバーフローを引き起こしたり、プログラムの実行効率が低下したりする可能性があります。
関数値
F# では、すべての関数が値と見なされ、実際に、関数値と呼ばれています。 関数は値であるため、他の関数の引数として使用したり、値が使用されるその他のコンテキストで使用したりできます。 関数値を引数として受け取る関数の例を次に示します。
let apply1 (transform : int -> int ) y = transform y
関数値の型を指定するには、-> トークンを使用します。 このトークンの左側に引数の型を、右側に戻り値の型を指定します。 上の例の apply1 は、関数 transform を引数として受け取る関数で、transform は、整数を受け取る、別の整数を返す関数です。 apply1 の使用方法を次のコードに示します。
let increment x = x + 1
let result1 = apply1 increment 100
このコードを実行すると、result の値が 101 になります。
複数の引数を指定するには、次のように、連続する -> トークンで区切ります。
let apply2 ( f: int -> int -> int) x y = f x y
let mul x y = x * y
let result2 = apply2 mul 10 20
結果は 200 になります。
ラムダ式
ラムダ式とは、名前のない関数です。 前の例では、名前のある関数 increment と mul を定義しましたが、次のように、代わりにラムダ式を使用することもできます。
let result3 = apply1 (fun x -> x + 1) 100
let result4 = apply2 (fun x y -> x * y ) 10 20
ラムダ式を定義するには、fun キーワードを使用します。 ラムダ式は関数定義に似ていますが、引数リストと関数本体の区切りに = トークンではなく -> トークンを使用します。 引数の型は、通常の関数定義と同様に、推論されるようにすることも、明示的に指定することもできます。戻り値の型も、本体の最後の式の型から推論されます。 詳細については、「ラムダ式: fun キーワード (F#)」を参照してください。
関数合成とパイプライン処理
F# の関数は、他の関数から合成することができます。 function1 と function2 という 2 つの関数を合成すると、function1 に続いて function2 を適用する別の関数になります。
let function1 x = x + 1
let function2 x = x * 2
let h = function1 >> function2
let result5 = h 100
結果は 202 になります。
パイプライン処理を使用すると、複数の関数呼び出しを一連の操作として連結することができます。 以下に例を示します。
let result = 100 |> function1 |> function2
この場合も、結果は 202 になります。
コンポジションの演算子は 2 個の関数を受け取り、関数を返します; 一方、パイプライン演算子は、関数引数と引数を受け取り、値を返します。 次のコード例は、関数の定義と使用の違いを参照することによって、パイプラインとコンポジションの演算子の違いを示します。
// Function composition and pipeline operators compared.
let addOne x = x + 1
let timesTwo x = 2 * x
// Composition operator
// ( >> ) : ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3
let Compose2 = addOne >> timesTwo
// Backward composition operator
// ( << ) : ('T2 -> 'T3) -> ('T1 -> 'T2) -> 'T1 -> 'T3
let Compose1 = addOne << timesTwo
// Result is 5
let result1 = Compose1 2
// Result is 6
let result2 = Compose2 2
// Pipelining
// Pipeline operator
// ( <| ) : ('T -> 'U) -> 'T -> 'U
let Pipeline1 x = addOne <| timesTwo x
// Backward pipeline operator
// ( |> ) : 'T1 -> ('T1 -> 'U) -> 'U
let Pipeline2 x = addOne x |> timesTwo
// Result is 5
let result3 = Pipeline1 2
// Result is 6
let result4 = Pipeline2 2
関数のオーバーロード
型、関数のメソッドをオーバーロードすることができます。 詳細については、「メソッド (F#)」を参照してください。