F# と Sodium でリアクティブ・プログラミング(2)
Sodium のCell
とアキュームレータの作成
Sodium のStream
が値を持つのはイベント発生の瞬間であるのに対して、Cell
はいったん獲得した値を保持できる。Cell
の値は書き換え可能で、次の書き換えまでその値を保持しており、いつでも自由に読み出すことができる。
本記事は SodiumFRP.FSharp を使用する。
以下に示すサンプルコードの動作は、
- コンソール画面で、キー入力を待つ
- 入力した文字をコンソール画面に出力する。ただし、数字とアルファベット以外のキー入力は無視する。
- 入力が数字キーならば、数字キーが累積で何回押下されたかをコンソール出力する。
- 入力がアルファベットキーならば、アルファベットキーが累積で何回押下されたかをコンソール画面に出力する。
- Esc キーを押せば、入力受付をやめてアプリケーションを終了する。
サンプルコード
サンプルコードの解説
/// キー入力イベント用ストリーム let sKeyInput: StreamSink<ConsoleKeyInfo> = StreamSink.create()
7行目:キー入力イベントを流すStreamSink<ConsoleKeyInfo>
としてsKeyInput
を作成(StreamSink
はStream
型の派生クラス)。StreamSink
型はsendS
関数(49行目で使用)を使って明示的にイベントを送信できる。
use lKeyInput: IStrongListener = sKeyInput |> mapS (fun (ki: ConsoleKeyInfo) -> match ki.KeyChar with | c when Char.IsNumber c -> Some c | c when Char.IsLetter c -> Some c | _ -> None) |> filterOptionS |> listenS (fun (c: char) -> printfn $" %c{c}")
9~17行目:キー入力イベント用ストリームsKeyInput
(StreamSink<ConsoleKeyInfo>
型)を、mapS
関数とfilterOptionS
関数を使ってStream<char>
に変換する。また、数字・アルファベット以外のキー入力イベントを破棄する。
更にlistenS
を使ってリスナー(IStrongListener
型)を作成(17行目)、イベントデータ処理用のリスナー関数を登録する。このリスナー関数では入力文字をそのままコンソール出力する。
/// 数字キーを押した回数を保持するセル let cNumberCounter: Cell<int> = loopWithNoCapturesC (fun (lpcCount: LoopedCell<int>) -> sKeyInput |> snapshotC lpcCount (fun (ki: ConsoleKeyInfo) (acc: int) -> if Char.IsNumber ki.KeyChar then acc + 1 else acc) |> calmS |> holdS 0)
20~26行目:loopWithNoCapturesC
関数を使って、数字キー押下の累積回数を保持するアキュームレータとなるcNumberCounter
(Cell<int>
型)を作成する。ストリームであるsKeyInput
にてイベントが発生する毎に、lpcCount
(LoopedCell<int>
型)の値を読み取り、数字キー入力と判定された場合のみカウンタ値をインクリメントして保持している。(LoopedCell
はCell
型の派生クラス)
calmS
関数を使うことにより、Stream
で発生したイベントの値が直前イベントと重複した場合にはイベントを破棄している。
/// アルファベットキーを押した回数を保持するセル let cLetterCounter: Cell<int> = sKeyInput |> accumS 0 (fun (ki: ConsoleKeyInfo) (acc: int) -> if Char.IsLetter ki.KeyChar then acc + 1 else acc) |> calmC
34~38行目:accumS
関数を使って、アルファベットキー押下の累積回数を保持するアキュームレータとなるcLetterCounter
(Cell<int>
型)を作成する。このとき、calmC
関数を使うことにより、Cell
の値の更新が発生しても既存の値から変化しないときはリスナー呼び出しを行わないように設定できる。
SodiumFRP.FSharp においては、アキュームレータを実現する方法は複数用意されている。本記事のサンプルコードのように、処理が単純なアキュームレータならば、21行目のloopWithNoCapturesC
を使うよりもaccumS
関数の方が簡潔な記述ができる。
※ソースコードはGitHubで公開しています
《参考資料》