この記事は F# Advent Calendar 2015 の16日目の記事です。 15日目は @yukitos さんの 「fsharp.orgステッカー配布中です」でした。
F#でFizzBuzz RX
@CallMeKohei さんの FizzBuzz! Fizzbuz! FizzBuzz! ~FizzBuzzだけでF#を効率的に学んでみた!~ が面白かったので便乗してみました。
先の記事では、手続き型・関数型・OOP・コンピュテーション式・ モナド 名前を言ってはいけないあの人・アクターモデルでの実装が挙げられていました。
まだ残っている実装方法で面白そうなの何かないかなーと考えながらラブライブ!サンシャイン!!の「君のこころは輝いてるかい?」を聴いてたら、「サンシャイン→太陽→太陽の子→RX……これだ!」とひらめきました。
というわけで、先の記事では出てなかった「RxでFizzBuzz」をやってみました。
なおF#には標準でObservableモジュールが用意されていますが機能がかなり限られていて不十分です。 FSharp.Control.Reactive を使用するとObservableを拡張してより一般的なRxの機能を使うことができるようになります。今回の実装でも FSharp.Control.Reactive を使用しました。
シンプルな実装
まずはシンプルに実装してみます。普通のFizzBuzzではスタート地点の「1, 2, 3, ...」という数列がforだったりシーケンスやリストになっているのですが、これをObservableに変えるだけでほとんど事足ります。
open FSharp.Control.Reactive
open FSharp.Control.Reactive.Observable
open System
module FizzBuzzBase =
/// 100ミリ秒ごとに1, 2, 3, ...を発行
let numberObservable =
TimeSpan.FromMilliseconds 100.0 |> interval |> map ((+) 1L)
|> publish
/// すべてのIConnectableObservableをいっせいにconnectする
let createStartFizzBuzz (observables : Lazy<IDisposable list>) () =
let disposables = observables.Force()
{ new IDisposable with member x.Dispose() = disposables |> List.iter (fun x -> x.Dispose()) }
/// FizzBuzzの基本関数
let fizzBuzz str d x = if x % d = 0L then Some str else None
/// アクティブパターン版
let (|FizzBuzz|_|) = fizzBuzz
module FizzBuzzRx1 =
open FizzBuzzBase
// 単純にmapの中でFizzBuzzのすべてを行う。あまりRxらしくない?
let outputObservable, startFizzBuzz =
let resultObservable =
numberObservable |> map (function
| (FizzBuzz "Fizz" 3L fizz) & (FizzBuzz "Buzz" 5L buzz) -> fizz + buzz
| FizzBuzz "Fizz" 3L fizz -> fizz
| FizzBuzz "Buzz" 5L buzz -> buzz
| x -> string x)
|> publish
let startFizzBuzz = lazy([resultObservable |> connect; numberObservable |> connect]) |> createStartFizzBuzz
resultObservable |> asObservable, startFizzBuzz
module Main =
open FizzBuzzRx1
[<EntryPoint>]
let main _ =
fizzBuzzObservable |> subscribe (printfn "%s") |> ignore
do
printfn "Enterを押すと終了します"
use doingFizzBuzz = startFizzBuzz()
Console.ReadLine() |> ignore
printfn "終了"
Console.ReadLine() |> ignore
0
コンパイルして実行すると、100ミリ秒ごとに1行ずつFizzBuzzが出力されます。
Fizz担当とBuzz担当に分離
上のコードはあまりRxらしくないと思います。結局1つしかないイベントの中身をmapしてるだけなので「それSeqでもできるよ」と言われてしまうとぐうの音も出ません。
数値列のイベントを監視しているやつを1人ではなく、Fizz担当とBuzz担当と数字担当(これは元の数値列を発行するObservableそのまま)に分けてみます。最終的に両者はzipで合流させています。
module FizzBuzzRx2 =
open FizzBuzzBase
/// FizzBuzzの基本関数
let fizzBuzz str d x = if x % d = 0L then Some str else None
/// 各FizzBuzzのパーツを結合して文字列を得る
let combineFizzBuzz = Seq.choose id >> String.concat ""
/// Fizzだけ担当するObservable
let fizzObservable = numberObservable |> map (fizzBuzz "Fizz" 3L)
/// Buzzだけ担当するObservable
let buzzObservable = numberObservable |> map (fizzBuzz "Buzz" 5L)
/// Fizz担当とBuzz担当の結果を結合するObservable
let fizzBuzzObservable = zipSeq [fizzObservable; buzzObservable] |> map combineFizzBuzz
// Fizz担当とBuzz担当と数字担当の結果を結合して最終結果を得る。
// 少しらしくなったけど、zipを多用してるのがいまいち
let fizzBuzzObservable, startFizzBuzz =
let resultObservable =
zip fizzBuzzObservable numberObservable
|> map (function "", num -> string num | fizzbuzz, _ -> fizzbuzz)
|> publish
let startFizzBuzz = lazy([resultObservable |> connect; numberObservable |> connect]) |> createStartFizzBuzz
resultObservable |> asObservable, startFizzBuzz
module Main =
open FizzBuzzRx2
// 以降同じなので省略
イベントが分離→合流して最終結果を出力しています。さっきの例よりはRxらしさが出てきたのではないかと思います。
ただ、この実装でいまいちなのはzipを多用している点です。それぞれの担当が対象外の数値の場合でも何らかの値を出力するようになっていて、イベントが合流する箇所では必ず何らかのイベントが毎回発行されることを期待しています。
つまり各担当が他の担当と協調するためにわざと出力タイミングを合わせるような実装になっていて、各担当のイベントの独立性が低くなっています。
また、時系列処理というRxらしい側面がほぼ生かされていないコードとなっているので、結局「それSeqでもできるよ」状態からはあまり脱却できていません。
Fizz担当、Buzz担当、数値担当をより独立させる
それぞれの担当は他の担当のことを気にせず、対象の数値の場合のみイベントを流す。それでも全イベントをうまく合流させることで最終的に期待する出力にする方がよりRxっぽいでしょう。
ある数値が発行されてから次の数値が発行されるまでの間に受信したイベントをひとまとめにし、その中に「Fizz」または「Buzz」の少なくとも片方があればそれらをすべて結合した文字列を、なければ数値イベントの値を最終結果として出力するようにすることで、それを実現しました。
module FizzBuzzRx3 =
open FizzBuzzBase
/// Fizzだけ担当するObservable。対象外の場合は何も出力しない。
let fizzObservable = numberObservable |> Observable.choose (fizzBuzz "Fizz" 3L)
/// Buzzだけ担当するObservable。対象外の場合は何も出力しない。
let buzzObservable = numberObservable |> Observable.choose (fizzBuzz "Buzz" 5L)
// 数値が発行されてから次の数値が発行されるまでの間に発生したイベントをひとまとめにして最終結果を得る。
// fizzまたはbuzzが発行されていればそちらを採用、されていなければ数値を採用する。(ambで実現)
// fizz担当、buzz担当が対象の数値の場合にだけイベントを発行すればよくなり、zipも必要なくなった。
let outputObservable, startFizzBuzz =
let numStrObservable = numberObservable |> map (string >> Choice2Of2)
let resultObservable =
mergeSeq [map Choice1Of2 fizzObservable; map Choice1Of2 buzzObservable; numStrObservable]
|> windowBounded numberObservable
|> bind (Observable.split id >> (<||) amb >> reduce (+))
|> publish
let startFizzBuzz = lazy([resultObservable |> connect; numberObservable |> connect]) |> createStartFizzBuzz
resultObservable |> asObservable, startFizzBuzz
module Main =
open FizzBuzzRx3
// 以降同じなので省略
これで完璧!というわけではなく、この実装にはまだ改善点が残っています。
今の実装はFizz→Buzzの順でイベントが発行される前提となっており、実際その順に発行されていますが、各担当の処理がすべて同じスレッド上で行われているからそうなっているにすぎないと思われます。各担当をマルチスレッド化するには、Fizz・Buzz・数値の各イベントが来るタイミングの制御を行う必要があるはずです。
また、一度各担当のイベントを合流させておいて、その後またFizzBuzzと数値に分離しているのもイマイチです。
今回はここまでにしますが、時間があれば上記改善を行ったバージョンも実装してみたいと思います。