概要
関数型リアクティブプログラミング(SodiumFRP)の練習として、アキュムレータロジックを構成するためのコードについて整理してみました。
アキュムレータ
プログラミングの世界のなかでのアキュムレータについてWikipediaでは、
演算装置による演算結果を累積する、すなわち総和を得るといったような計算に使うレジスタや変数のことである。
・・・のように説明しています。
これを、SodiumFRPの世界・用語で説明すると、既存の情報(セルの値)と、新しい情報(ストリームの値)を組み合わせによって更新される値を持つ情報コンテナ(セル)となります。
例えば、値を合計していくアキュムレータ(セル)は、初期値「0」を与えておいたうえで、順に「5」「20」「10」という値のストリームを受けると、その値を「0(初期値)」→「5」→「25」→「35」と更新するように振舞います。
このような値を合計していくアキュムレータについて(プリミティブな要素の組合せで)どのようにロジックを構成すればよいか?を、書籍『関数型リアクティブプログラミング』@技術評論社では、Java + SodiumFRP でのコードを使いながら丁寧に解説しています(セクション2.9)。
ここでは、F# + SodiumFRP で同様のアキュムレータを組み上げるコードを複数パターン示していきます。
(1) CellLoop + snapshotC で構成
書籍『関数型リアクティブプログラミング』で示しているコードを単純にF#に書き換えたものになります。なお、F#では先頭文字以外ではシングルクォーテーションを変数名に利用することができます。シングルクォーテーションが付いたsum'
という変数が特別な意味を持つわけではありません。
open System
open SodiumFRP
open System.Threading
let str o = o.ToString()
[<EntryPoint>]
let main argv =
let sUserInput = StreamSink.create<int> ()
(* アキュムレータを構成 *)
let sum = Transaction.run( fun () ->
let sum' = CellLoop<int> ()
let sUpdate = sUserInput
|> snapshotC sum' (fun s v -> s + v)
do sum'.Loop( sUpdate |> holdS 0 )
sum'
)
(* ここまで *)
let l = sum
|> listenC (fun s -> Console.WriteLine("更新 sum -> " + (str s)))
Thread.Sleep(1000)
sUserInput |> sendS 5 // 値5を内容とするストリーム(sUserInput)を発火
Thread.Sleep(1000)
sUserInput |> sendS 20
Thread.Sleep(1000)
sUserInput |> sendS 10
do Console.ReadKey() |> ignore
do l.Unlisten()
0
ここでsUserInput |> sendS 5
は、GUIなどを通してのユーザから入力イベントを模しています。これを実行すると、結果は次のようになります。
更新 sum -> 0
更新 sum -> 5
更新 sum -> 25
更新 sum -> 35
まず、すぐに更新 sum -> 0
が出力され、1秒後に更新 sum -> 5
、さらに1秒後に更新 sum -> 25
、さらに1秒後に更新 sum -> 35
が表示されます。ユーザからの入力に対してリアクティブな(反応的な)プログラムになっていることが分かります。
(2) CellLoop + mapS + sampleC で構成
次のコードは、shapsotC
関数の代わりにsampleC
関数を使った例です。(1)で示したコードの(* アキュムレータを構成 *)
から(* ここまで *)
の部分を差し替えます。
let sum = Transaction.run( fun () ->
let sum' = CellLoop<int> ()
let sUpdate = sUserInput
|> mapS (fun v -> (sampleC sum') + v)
do sum'.Loop( sUpdate |> holdS 0 )
sum'
)
実行結果は(1)と同じです。
(3) loopWithNoCapturesC + snapshotC で構成
(1)と(2)のコードでは、do sum'.Loop( sUpdate |> holdS 0 )
という部分が(sum'
というオブジェクトの内部状態の変更を行なっていることから)関数型プログラミングらしくないので、これを内部に隠蔽したものが次のloopWithNoCapturesC
関数になります。
let sum = loopWithNoCapturesC ( fun sum' ->
let sUpdate = sUserInput
|> snapshotC sum' (fun s v -> s + v)
sUpdate |> holdS 0 )
実行結果は(1)と同じです。
(4) loopWithNoCapturesC + mapS + sampleC で構成
let sum = loopWithNoCapturesC ( fun sum' ->
let sUpdate = sUserInput
|> mapS (fun v -> (sampleC sum') + v)
sUpdate |> holdS 0 )
実行結果は(1)と同じです。
(5) accumS で構成
ヘルパー関数accumS
を使用した例です(この関数は内部的にはloopとsnapshotを使って実装されているようです)。非常にシンプルに書けます。
let sum = sUserInput |> accumS 0 (fun s v -> s + v)
実行結果は(1)と同じです。
(6) 悪い例
let mutable exSum = 0 // 可変な変数
let sum = sUserInput
|> mapS ( fun v ->
exSum <- exSum + v
exSum )
|> holdS exSum
実行結果は(1)と同じなのですが、これはFRP外部にある可変変数exSum
について読み取り&書き替えをしており(mapSの引数に渡される関数(ラムダ式)が参照透過的ではない)、FRPが保証してくれるはずの合成性を破綻させてしまう非常に悪い例です。