この記事は Hello World あたたたた Advent Calendar 2025 の25日目の記事です。
概要
こちらを参照。
面白そうだったので、F#を使い、しかしレギュレーションから外れて「処理フロー」から逸脱したロジックで実装してみました。
実装
open System
let solve =
let rec loop cnt flg (rnd : Random) =
seq {
match cnt with
| 4 when flg -> yield ""
| _ ->
let (ata, nf, nc) = if rnd.Next(2) = 0 then ("あ", true, 0) else ("た", flg, cnt + 1)
yield ata
yield! loop nc nf rnd
}
loop 0 false (new Random())
solve
|> String.concat ""
|> printfn "%s"
printfn "お前はもう死んでいる"
実行結果
解説
open
まずは乱数生成に必要なRandomクラスを使うためにSystem名前空間をオープンします。
open System
関数solve
この関数では「あ」と「た」からなるシーケンスを作成しています。
関数の中に関数を定義していて、中身の関数は再帰関数になっています。この再帰関数がほぼ本体です。
繰り返し(再帰処理)
let rec loop cnt flg (rnd : Random) =
seq {
match cnt with
| 4 when flg -> yield ""
| _ ->
let (ata, nf, nc) = if rnd.Next(2) = 0 then ("あ", true, 0) else ("た", flg, cnt + 1)
yield ata
yield! loop nc nf rnd
}
loopという関数名の通り、ループするために定義した関数です。今回whileでループせず再帰ループする点がレギュレーションから外れている点ですね。
この関数の引数はそれぞれ、cntが「た」の連続出現数、flgが「あ」が出現したか、rndが乱数生成用のインスタンスです。
そして戻り値としてシーケンスseqを返します。シーケンスはC#で言うとIEnumerableみたいなものですね。
シーケンスの中身ですが、match式によって処理を分岐しています。ifでもいいのですが、パターンマッチングというF#の機能をあえて使っています。
条件分岐
match cnt with
| 4 when flg -> yield ""
| _ ->
let (ata, nf, nc) = if rnd.Next(2) = 0 then ("あ", true, 0) else ("た", flg, cnt + 1)
yield ata
yield! loop nc nf rnd
パターンは2パターン。cntの値が4でありflg = trueであるか、そうでないか。正直if elseで十分ですね。
前者の場合、シーケンス要素として""(空文字列)を返し、そこで処理が終わります(=シーケンスの終端)。
if rnd.Next(2) = 0 then ("あ", true, 0) else ("た", flg, cnt + 1)
後者の場合、まずはrnd.Next(2)で0か1の乱数を生成し、その結果によって後続処理で使う値を決めています。
F#の場合、ifは「if文」ではなく「if式」と呼ばれ、式になっていますので戻り値を持っていて、その結果を変数に入れることができます。これはC#でいう三項条件演算子? :に相当します。
乱数が0の場合、「あ」として処理します。そのため、出力用の「あ」と、出現フラグとしてtrue、そして「た」の連続数を0にリセットします。
1の場合は「た」として処理し、出力用に「た」、出現フラグは関数の引数をそのまま継承し、連続数に1を加算します。
文字の出力と再帰呼び出し
yield ata
yield! loop nc nf rnd
そして出力用の値を入れた変数ataをyieldで返します。
最後にyield!ですが、これは後続のシーケンスの中身をそれぞれリターンします。ここではloop関数を再帰呼び出しして次のシーケンスを生成していますので、その結果が順次リターンされます。
再帰関数を定義したら、最後にそれを呼び出します。
loop 0 false (new Random())
関数呼び出しとパイプライン
solve
|> String.concat ""
|> printfn "%s"
ここでは定義した関数solveを呼び出しています。
次の行にある|>はパイプライン演算子で、左側にある値を右側の関数の引数に渡す演算子です。読みやすさのために改行していますが、solve |> String.concat ""のように一行で書くこともできます。
String.concat関数は2つの引数を取ります。第1引数が「文字列同士を繋ぐセパレータ」で、第2引数が「string型のシーケンス」です。
基本的に|>演算子の右側に来る関数は「引数が1つしかない関数」です。よってString.concat ""というように、第1引数の""も含めた部分を1つの関数と捉えます。所謂カリー化というやつです。
そしてString.concat ""の戻り値はすべてのstringを連結した文字列になりますので、続くprintfn "%s"にその戻り値を渡して「あたたたた」を出力します。
printfn自体は1個以上の引数を取る関数で、第1引数に出力する文字列のフォーマットを指定します。%sは文字列を表す書式になっているので、第2引数として文字列を受け取るようになります。よってprintfn "$s"という関数は「文字列1つを受け取ってそれを出力する関数」になります。
最後
printfn "お前はもう死んでいる"
最後に決めゼリフ。
おわりに
というわけでwhileループしない実装をしてみました。
念のため言っておくと、F#にもwhile式はあります。ただその場合、変数を書き換えていくような実装になると思います。
F#の変数はミュータブルにもできるのですが、基本はイミュータブルなので、変数はイミュータブルにしたまま実装しようとした結果がこの形でした。
こんな感じの縛りプレイでコードを書いてみると、いつもと違った何かが見えてくるかもしれません。皆さんも是非。
