再帰関数: rec キーワード
rec
キーワードは、let
キーワードと共に使用して、再帰関数を定義します。
構文
// Recursive function:
let rec function-name parameter-list =
function-body
// Mutually recursive functions:
let rec function1-name parameter-list =
function1-body
and function2-name parameter-list =
function2-body
...
解説
再帰関数 (それ自身を呼び出す関数) は、F# 言語では、rec
キーワードを使用して明示的に識別します。 rec
キーワードを使用すると、let
バインドの名前をその本体で使用できるようになります。
次の例は、数学的な定義を使用して n番目のフィボナッチ数を計算する再帰関数を示しています。
let rec fib n =
match n with
| 0 | 1 -> n
| n -> fib (n-1) + fib (n-2)
Note
実際には、前のサンプルのようなコードは、既に計算されている値を不必要に再計算しているため、理想的ではありません。 これは、この記事で詳しく説明されている末尾再帰ではないためです。
メソッドは、それが定義されている型内で暗黙的に再帰になるため、rec
キーワードを追加する必要はありません。 次に例を示します。
type MyClass() =
member this.Fib(n) =
match n with
| 0 | 1 -> n
| n -> this.Fib(n-1) + this.Fib(n-2)
ただし、クラス内の let
バインドは暗黙的に再帰ではありません。 すべての let
バインド関数には rec
キーワードが必要です。
末尾再帰
再帰関数によっては、より "純粋な" 定義を末尾再帰にリファクターする必要があります。 これにより、不必要な再計算が防止されます。 たとえば、前のフィボナッチ数ジェネレーターは、次のように書き換えることができます。
let fib n =
let rec loop acc1 acc2 n =
match n with
| 0 -> acc1
| 1 -> acc2
| _ ->
loop acc2 (acc1 + acc2) (n - 1)
loop 0 1 n
フィボナッチ数の生成は、数学的には純粋であるものの実際には非効率である "単純な" アルゴリズムの好例です。 これはより複雑な実装ですが、F# では、再帰的に定義されたままであり、いくつかの面で効率的です。
loop
という名前の再帰的な内部関数。これは F# の慣用的なパターンです。- 2 つのアキュムレータ パラメーター。これらは累積値を再帰呼び出しに渡します。
n
の値をチェックして、特定の累積値を返します。
ループを使用して反復的にこの例を記述したとすると、そのコードは、特定の条件が満たされるまで 2 つの異なる数値を累積していくものになります。
これが末尾再帰であると言えるのは、この再帰呼び出しでは、コール スタックに値を保存する必要がないためです。 計算されるすべての中間値は、内部関数への入力によって累積されます。 こうすれば、while
ループのようなものを記述した場合と同様に高速になるよう、F# コンパイラでコードを最適化することもできます。
前の例で示したように、内部および外部関数を使用して何かを再帰的に処理する F# コードを記述するのは、一般的なことです。 内部関数では末尾再帰を使用しますが、外部関数は呼び出し元に対してより適切なインターフェイスを備えています。
F# 8.0 以後では、TailCall
属性を 使用して、末尾再帰関数を定義する意図を、コンパイラに対し明示的に伝えることができます。 その後は、関数が末尾再帰以外の呼び出しを行う場合には、コンパイラが警告を表示します。 この属性は、メソッドおよびモジュール レベルの関数で使用できます。
たとえば、最初の fib
の定義で使用するとします。
[<TailCall>]
let rec fib n =
match n with
| 0 | 1 -> n
| n -> fib (n-1) + fib (n-2)
この場合、2 つの非末尾再帰呼び出しに関するコンパイラ警告がトリガーされます。
相互再帰関数
場合によっては、関数が "相互再帰" になることがあります。これは呼び出しが環状になって、最初に呼び出した別の関数から、今度は自分が呼び出されることです。両者の間の呼び出し回数は任意です。 このような関数は、1 つの let
バインドの中で一緒に定義し、and
キーワードを使用してそれらをリンクする必要があります。
次の例は、2 つの相互再帰関数を示しています。
let rec Even x = if x = 0 then true else Odd(x - 1)
and Odd x = if x = 0 then false else Even(x - 1)
再帰的な値
let
バインドの値が再帰的になるように定義することもできます。 これは、ログ記録のために行われることがあります。 F# 5 と nameof
関数を使用すると、次の操作を実行できます。
let rec nameDoubles = nameof nameDoubles + nameof nameDoubles
関連項目
.NET