FizzBuzz を使って F# を紹介する
はじめに
昔からある最低限のプログラムが書けるふるいとしての役割があるらしい「FizzBuzz 問題」をやりながら、アクティブパターンなどの便利な機能について触れながら F# を紹介します。
なお、 F# の紹介と言いつつ F# の構文についての説明はありません。 F# 初心者もしくは未知のプログラミング言語を見たときなんとなく処理が追えるくらいの方が想定読者です。
FizzBuzz 問題
FizzBuzz 問題とは以下のような問題です。
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
「どうしてプログラマに・・・プログラムが書けないのか?」(原典: "Using FizzBuzz to Find Developers who Grok Coding")
本記事では最低限のプログラムを書くことが目的ではないし、読者を FizzBuzz 問題が解けるかどうかのレベルではないと想定しているので、途中のステップはある程度飛ばして、以下のような問題にします。
整数 n が与えられたとき、 n が 3 の倍数なら Fizz、 n が 5 の倍数なら Buzz、 n が 3 の倍数かつ 5 の倍数なら FizzBuzz、それ以外の場合は n を出力する関数 printFizzBuzz を作ること。
ちなみに F# で条件を満たす関数 printFizzBuzz が作成できれば、以下のように原題に答えることができます。
List.iter printFizzBuzz [1..100]
本文中の記法
記法 | 意味 |
---|---|
型 -> 型 | 左の型を引数として右の型を返す関数の型表現 |
最初のステップ
愚直に条件を書けば以下のようになります。
let printFizzBuzz = function
| n when n % 3 = 0 && n % 5 = 0 -> printfn "FizzBuzz"
| n when n % 3 = 0 -> printfn "Fizz"
| n when n % 5 = 0 -> printfn "Buzz"
| n -> printfn "%d" n
余談ですが F# の printfn は静的型チェックがあり、上記 printfn "%d" は int -> unit 型(unit は何もないことを表す唯一の値からなる型)となって n が int 型でなければコンパイルエラーになります。便利ですね。
アクティブパターンの導入
最初の printFizzBuzz にアクティブパターンを導入します。アクティブパターンは以下のようにパターンマッチで利用できるパターンを作れる機能です(最大 7 パターン)。
let (|FizzBuzz|Fizz|Buzz|Number|) = function
| n when n % 3 = 0 && n % 5 = 0 -> FizzBuzz
| n when n % 3 = 0 -> Fizz
| n when n % 5 = 0 -> Buzz
| n -> Number(n)
let printFizzBuzz = function
| FizzBuzz -> printfn "FizzBuzz"
| Fizz -> printfn "Fizz"
| Buzz -> printfn "Buzz"
| Number(n) -> printfn "%d" n
アクティブパターンの実体は関数です。アクティブレコグナイザーと言われます。関数である以上はもともとのロジックがそのまま流用できます。最初の fizzbuzz が printfn であったのに対し、アクティブレコグナイザーでは(パラメーター付き)パターンを返すことができます。
最初のステップで作成した fizzbuzz 関数が各パターンが複雑な条件を記述しているのに対し、アクティブパターンを導入したことによって、それぞれのパターンの意味が明確になりました。
さらに以下のように一旦文字列に変換する関数を挟むこともできます。
let toFizzBuzzString = function
| FizzBuzz -> "FizzBuzz"
| Fizz -> "Fizz"
| Buzz -> "Buzz"
| Number(n) -> string n
let printFizzBuzz = toFizzBuzzString >> printfn "%s"
>>
は関数合成演算子です。つまり左オペランドの関数を評価して右オペランドの関数を評価する関数を作ります。ちなみに本文には出てきませんが、左オペランドを右オペランドの引数に与えるパイプ演算子(|>
)も F# ではよく利用されます。
さて、ここまでで気が付くのは以下の 3 点です。
- アクティブパターンを定義することにより、パターンごとに名前が付きわかりやすくなる
- アクティブパターンのパターンにはパラメーターを定義でき、アクティブパターンを利用したパターンマッチで利用できる
- アクティブパターン利用することで、同じパターンマッチを利用した後続ロジックの差し替えが容易になる
これだけでもかなり便利なのですが、さらに続けます。
パーシャルアクティブパターンの導入
アクティブパターンが入力に対する完全なパターンマッチを定義するのに対して、パーシャルアクティブパターンは入力の一部のみがパターンになるようなものです。アクティブレコグナイザーの最後のパターンをワイルドカード _ にして結果を option 型にします。
let (|Fizz|_|) = function
| n when n % 3 = 0 -> Some()
| _ -> None
let (|Buzz|_|) = function
| n when n % 5 = 0 -> Some()
| _ -> None
let toFizzBuzzString = function
| Fizz & Buzz -> "FizzBuzz"
| Fizz -> "Fizz"
| Buzz -> "Buzz"
| n -> string n
let printFizzBuzz = toFizzBuzzString >> printfn "%s"
ここでは Fizz/Buzz に該当するときに(アクティブパターンにマッチするときに)返すべき値がないので unit 型のリテラルである () を Some に与えて結果として返しています。 toFizzBuzzString 関数は先ほどの定義とほぼ同じですが、最後を変数パターン(n)で受けているところだけ少しだけ変わっていることに注意してください。
先ほどのアクティブパターンでは最後のパターンが数値であることが意味的に明確でしたが、パーシャルアクティブパターンではパターンから漏れたもの(FizzBuzz でも Fizz でも Buzz でもないもの)という少しわかりづらいものになったように見えるかもしれません。しかしパーシャルアクティブパターンを利用したことにより、アクティブレコグナイザー内のパターンが分解されたことは注目に値します。
パターンが分解されると何がうれしいのでしょうか。
まず単体テストが単純になるという点が挙げられます。元のアクティブパターンでは条件分岐が複雑である分テストが複雑になりますが、パーシャルアクティブパターンで定義した単純なアクティブレコグナイザーはテストが容易です[1]。
次に、関数の組み換えが容易になるということです。上記の toFizzBuzzString は分解したパターンを再度結合しているパターンが複雑化していますし、変数パターンで受けた数値を文字列に変換してしまうのは情報が失われてもったいないです。
パターンの分解と関数の組み換えを意識しつつさらに printFizzBuzz を書き換えていきます。
もっと関数型っぽくしたい
Fizz も Buzz も FizzBuzz も特定の数の倍数であるときに特定の文字列を返し、そうでないときは何も行いません。言い換えると、倍数チェックに成功したときに特別な状態になり、失敗の時は元の状態であるような関数です。これを表す関数は次のように定義できます。
let (|DivisibleBy|_|) divisor = function
| n when n % divisor = 0 -> Some()
| _ -> None
let tryDivide divisor success = function
| DivisibleBy divisor -> Ok(success)
| n -> Error(n)
アクティブレコグナイザーは関数なので、引数をとることができます。これはパラメーター化されたアクティブパターンと言われます。 tryDivide 関数でパラメーター化されたアクティブパターンに、除数を渡すことによって FizzBuzz の倍数チェックと特定の文字列を返すようなことを実現しています。
これを使えば、 Fizz、 Buzz、 FizzBuzz に相当する処理は以下のように書けます。
let tryFizz = tryDivide 3 "Fizz"
let tryBuzz = tryDivide 5 "Buzz"
let tryFizzBuzz = tryDivide (3 * 5) "FizzBuzz"
いずれも int -> Result<string, int> という型になっており、整数を受け取って 1 つ目のの引数で割り切れれば成功として 2 つ目の引数の文字列を返し、そうでなければ失敗として与えられれた整数を返します。
このままだと型の不一致で関数の結合はできないので、以下の補助関数を用意します[2]。
let either fOk fError = function
| Ok(x) -> fOk x
| Error(x) -> fError x
either は成功時に 1 つ目の引数の関数を実行し、失敗時に 2 つ目の引数の関数を実行します。
either を利用して、成功時、つまり前の処理で既に文字列になった場合はそのまま成功とし、失敗時、つまり前の処理で整数のままである場合は tryFizz/Buzz 関数を実行するようにするには、以下のようにします[3][4]。
let toFizzBuzzString =
tryFizzBuzz
>> either Ok tryFizz
>> either Ok tryBuzz
ここで toFizzBuzzString の型は int -> String ではなく int -> Result<string, int> であることに注意してください。
ちなみに F# では演算子も関数であり、演算子の定義もできるので、途中から either Ok なる表記が現れて対称性が崩れて気持ち悪い人は以下のようにすると見た目がすっきりします。
let (>=>) f g = f >> either Ok g
let toFizzBuzzString =
tryFizzBuzz
>=> tryFizz
>=> tryBuzz
最終的には出力したいので、最後に出力処理をつなげて完成です。
let printFizzBuzz =
toFizzBuzzString
>> either (printfn "%s") (printfn "%d")
ちなみにこのような、処理の成功・失敗に注目して関数を組み合わせていく方法を鉄道指向プログラミング(railway oriented programming)と言います。詳しくは参考文献をご覧ください。
おわりに
FizzBuzz 問題をテーマに F# の紹介をしました。アクティブパターンは F# の機能の中でも特に便利なものなので F# を使う人は積極的に採用していきましょう。関数を分解したり組み合わせたりするのが楽しくなってくると実用的なプログラミングができるようになっていきます。
実際の開発だとあらゆるところにエラーの可能性があるためエラー処理に頭を悩ませることが多いですが、鉄道指向プログラミングはとても簡単にエラー処理が実現できます。なお、本文では失敗側のフローをメインフローとしているため either のメイン処理が失敗側になっていますが、一般的には成功側をメインフローとするのが普通です。その点はお含みおきください。
参考文献
以下は本記事とほぼ同一題材を扱っている英語記事です。本記事で割愛した 15 の倍数の分岐を除いて別の因数のパターンを拡張できるような発展についても触れられています。
以下は前の参考文献の鉄道指向プログラミングに関する別の記事を有志の方が翻訳したものです。
-
アクティブレコグナイザーは実体が関数なので、普通の関数と同じようにテスト可能です。 ↩︎
-
FSharpPlus というライブラリーに Result.either という同名の関数が定義されています。このくらいは標準モジュールにもあってもよさそうなのですが。。。 ↩︎
-
FSharpPlus には either Ok と同等な Result.bindError という関数があります。 ↩︎
-
Ok はコンストラクターですが、コンストラクターは関数のように扱うことができます。 ↩︎
Discussion