LoginSignup
0
2

More than 5 years have passed since last update.

[SodiumFRP + F#] アキュムレータロジックを構成するいくつかの方法

Last updated at Posted at 2019-02-15

概要

関数型リアクティブプログラミング(SodiumFRP)の練習として、アキュムレータロジックを構成するためのコードについて整理してみました。

アキュムレータ

プログラミングの世界のなかでのアキュムレータについてWikipediaでは、

演算装置による演算結果を累積する、すなわち総和を得るといったような計算に使うレジスタや変数のことである。

・・・のように説明しています。

これを、SodiumFRPの世界・用語で説明すると、既存の情報(セルの値)と、新しい情報(ストリームの値)を組み合わせによって更新される値を持つ情報コンテナ(セル)となります。

例えば、値を合計していくアキュムレータ(セル)は、初期値「0」を与えておいたうえで、順に「5」「20」「10」という値のストリームを受けると、その値を「0(初期値)」→「5」→「25」→「35」と更新するように振舞います。

このような値を合計していくアキュムレータについて(プリミティブな要素の組合せで)どのようにロジックを構成すればよいか?を、書籍『関数型リアクティブプログラミング』@技術評論社では、Java + SodiumFRP でのコードを使いながら丁寧に解説しています(セクション2.9)。

ここでは、F# + SodiumFRP で同様のアキュムレータを組み上げるコードを複数パターン示していきます。

(1) CellLoop + snapshotC で構成

書籍『関数型リアクティブプログラミング』で示しているコードを単純にF#に書き換えたものになります。なお、F#では先頭文字以外ではシングルクォーテーションを変数名に利用することができます。シングルクォーテーションが付いたsum'という変数が特別な意味を持つわけではありません。

CellLoop+snapshotCで構成
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)で示したコードの(* アキュムレータを構成 *)から(* ここまで *)の部分を差し替えます。

CellLoop+mapS+sampleCで構成
  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関数になります。

loopWithNoCapturesC+snapshotCで構成
  let sum = loopWithNoCapturesC ( fun sum' ->
    let sUpdate = sUserInput 
                |> snapshotC sum' (fun s v -> s + v) 
    sUpdate |> holdS 0 )

実行結果は(1)と同じです。

(4) loopWithNoCapturesC + mapS + sampleC で構成

loopWithNoCapturesC+sampleCで構成
  let sum = loopWithNoCapturesC ( fun sum' ->
    let sUpdate = sUserInput 
                |> mapS (fun v -> (sampleC sum') + v) 
    sUpdate |> holdS 0 )

実行結果は(1)と同じです。

(5) accumS で構成

ヘルパー関数accumSを使用した例です(この関数は内部的にはloopとsnapshotを使って実装されているようです)。非常にシンプルに書けます。

accumSで構成
  let sum = sUserInput |> accumS 0 (fun s v -> s + v)  

実行結果は(1)と同じです。

(6) 悪い例

accumSで構成
  let mutable exSum = 0  // 可変な変数
  let sum = sUserInput 
          |> mapS ( fun v -> 
                    exSum <- exSum + v
                    exSum )
          |> holdS exSum 

実行結果は(1)と同じなのですが、これはFRP外部にある可変変数exSumについて読み取り&書き替えをしており(mapSの引数に渡される関数(ラムダ式)が参照透過的ではない)、FRPが保証してくれるはずの合成性を破綻させてしまう非常に悪い例です。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2