Skip to content

step9 再帰関数によるループ処理

このステップでは、これまで while 式で表現していたループ処理を、再帰関数を使って書き直す。 F# における再帰処理の基本的な考え方を理解し、再帰を使った関数型プログラミングのスタイルを身につけよう。

再帰関数とは

再帰関数とは、自分自身の処理の中で自分自身を呼び出す関数のことである。 再帰関数を使うと、while 式などのループ構文を使わずに、繰り返し処理を表現できる。

例えば、リストの要素の合計を計算する関数 sum は、再帰的に定義できる。

let rec sum list =
match list with
| [] -> 0
| head :: tail -> head + sum tail

sum 関数は、与えられたリストが空リスト [] の場合は 0 を返し、それ以外の場合は先頭要素 head と残りのリスト tail の合計を返す。 残りのリストの合計を計算するために、sum 関数自身を呼び出していることがわかる。 リストの :: 演算子は、リストの先頭に要素を付加した新しいリストを作成する。

再帰関数を定義する際は、let ではなく let rec を使う必要がある。 rec キーワードは、関数が再帰的であることをコンパイラに伝える役割がある。

ループ処理を再帰関数で書き直す

数当てゲームのループ処理を、再帰関数を使って書き直してみよう。 まず、ループの中心となる処理を gameLoop という名前の関数として定義する。

let rec gameLoop answer =
printfn "数字を入力してください:"
let inputString = stdin.ReadLine()
let inputNumber = int inputString
let result = judge answer inputNumber
match result with
| TooBig ->
printfn "大きすぎます!"
gameLoop answer
| TooSmall ->
printfn "小さすぎます!"
gameLoop answer
| Correct ->
printfn "正解!"

gameLoop 関数は、正解の数 answer を引数として受け取る。 関数内では、まずユーザーに数字の入力を促す。 次に、入力された数字を judge 関数を使って判定し、結果に応じてメッセージを表示する。 結果が Correct でない場合、gameLoop 関数自身を answer を引数として再度呼び出す。 これにより、ユーザーが正解するまで処理が繰り返される。

gameLoop 関数を定義したら、Program.fswhile ループの部分を、gameLoop 関数の呼び出しに置き換える。

let random = Random()
let answer = random.Next(1, 101)
printfn "正解は%dです" answer
gameLoop answer

gameLoop 関数は answer を引数として受け取るので、answer を渡している。

Program.fs の全体像

ここまでの変更を適用した Program.fs の全体像は以下のようになる。

open System
type GameResult =
| TooBig
| TooSmall
| Correct
let judge answer input =
match compare input answer with
| 1 -> TooBig
| -1 -> TooSmall
| _ -> Correct
let rec gameLoop answer =
printfn "1から100までの数字を入力してください。:"
let inputNumber = stdin.ReadLine() |> int
let result = judge answer inputNumber
match result with
| TooBig ->
printfn "大きすぎます!"
gameLoop answer
| TooSmall ->
printfn "小さすぎます!"
gameLoop answer
| Correct ->
printfn "正解!"
let answer = Random().Next(1, 101)
printfn "正解は%dです" answer
gameLoop answer

書き換えたら dotnet run で実行してみよう。 while ループを使った場合と同じように、ユーザーが正解するまで繰り返し入力を促されるはずだ。

再帰関数の注意点

再帰関数は、ループ処理を表現する強力な方法だが、注意すべき点もある。 再帰呼び出しが深くなりすぎると、スタックオーバーフローエラーが発生する可能性がある。

例えば、以下の infiniteRecursion 関数は、永遠に自分自身を呼び出し続けるため、最終的にスタックオーバーフローエラーとなる。

let rec infiniteRecursion () =
infiniteRecursion ()
infiniteRecursion () // スタックオーバーフローエラーが発生

再帰関数を設計する際は、必ず停止条件を設けることが重要だ。 gameLoop 関数の場合、resultCorrect になったときに再帰呼び出しが停止するようになっている。

まとめ

このステップでは、以下の内容を学んだ。

  • 再帰関数の定義方法
  • let rec キーワードの使用
  • 再帰関数を使ったループ処理の表現
  • gameLoop 関数の作成と while ループの置き換え
  • 再帰関数の注意点と末尾再帰
  • リストを使った再帰関数の例 (sum 関数)

再帰関数を使うことで、while ループを使わずに繰り返し処理を表現できた。 F# では、再帰はループを表現する一般的な方法であり、関数型プログラミングの重要な要素である。 再帰関数を使いこなすことで、より F# らしいコードを書くことができるようになるだろう。

このステップで、数当てゲームは一通り完成した。 次のステップでは、ゲームのルール説明を追加するなどして、ユーザビリティを向上させよう。