SodiumFRP + WPF(F#) ※ ReactivePropertyとの比較付き

# 概要
　初期値50から開始して45から55の範囲で、「+1」ボタンによりカウントアップ、「-1」ボタンによりカウントダウンするアプリケーションを、SodiumFRPを利用したものと、ReactivePropertyを利用したものの2つのバージョンで作成しました。
- [SodiumFRP](https://github.com/SodiumFRP/sodium) + WPF + F# による MVVM なスタイル
- ReactiveProperty + WPF + F# による MVVM なスタイル

## 動作のイメージ
ボタンをクリックすることでカウントアップ、カウントダウンしていきます。
![2019-02-05_12h18_58.png](https://qiita-image-store.s3.amazonaws.com/0/152805/7f71370d-3cc9-219d-ab1f-3ac4ab447615.png)

範囲（45～55）を超える数値とならないように、ボタンの有効・無効も`ICommand`の`CanExecute`を通じて自動で切り替わるようにしていきます。
![2019-02-05_12h28_59.png](https://qiita-image-store.s3.amazonaws.com/0/152805/fe974a73-4f7d-33dd-5ad2-85b009b49f96.png)

# SodiumFRP

SodiumFRPとは・・・、説明できるほど理解しきれていません。

- セル（Cell）とは「状態」をモデル化したもの
- ストリーム（Stream）とは「状態の変化」をモデル化したもの

関数型リアクティブプログラミング (翔泳社) や、[Sodium(FRP)を使ってみた@Qiita](https://qiita.com/mishima/items/f242808c76fd5b4cf3b1)、[SodiumでFunctional Reactive Programming (F#)@何でもプログラミング](http://any-programming.hatenablog.com/entry/2017/01/28/162128)が参考になります。

# 準備（共通）
F#でWPFアプリケーションを作成するための手順については、[F#+WPF+ReactiveProperty@Qiita](https://qiita.com/ozwk/items/cdf0072f4a04f158490e) を参考にさせていただきました。C#のときのように、VisualStudioの「新規プロジェクト作成」からテンプレートを選んで・・・というわけにはいかないようです（VS2017の場合）。

## Viewの設計（共通）

- データコンテキストとして、次セクションで作成するビューモデル`MainWindowVM (.fs)`を指定
- カウント値を表示するためのTextBlockを配置して、プロパティ`Count.Value`をバインディング
- カウントアップ／ダウンの操作のボタンを配置して、コマンド`PlusCommand`、`MinusCommand`をバインディング
  - コマンドパラメータは「なし」としておきます。 

```xml:MainWindow.xaml
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vm ="clr-namespace:xxxxLab.ViewModels;assembly=WindowApp"
    Title="{Binding WindowTitle, Mode=OneTime}" Height="85" Width="350">
  <Window.DataContext>
    <vm:MainWindowVM/>
  </Window.DataContext>
  <Grid>
    <StackPanel Orientation="Horizontal" Margin="10">
      <StackPanel.Resources>
        <Style TargetType="{x:Type TextBlock}">
          <Setter Property="Width" Value="60"/>
          <Setter Property="TextAlignment" Value="Center"/>
          <Setter Property="VerticalAlignment" Value="Center"/>
          <Setter Property="FontSize" Value="18"/>
        </Style>
        <Style TargetType="{x:Type Button}">
          <Setter Property="Width" Value="50"/>
          <Setter Property="Height" Value="25"/>
          <Setter Property="Margin" Value="5,0"/>
        </Style>
      </StackPanel.Resources>
      <TextBlock Text="{Binding Count.Value}"/>
      <Button Content="+1" Command="{Binding PlusCommand}"/>
      <Button Content="-1" Command="{Binding MinusCommand}"/>
    </StackPanel>
  </Grid>
</Window>
```

## ViewModelの設計 ReactivePeroperty版

- Nugetで「ReactiveProperty 5.3.2」を追加
- ReactiveCommand.Subscribeの戻値はとりあえず破棄

``` FSharp:MainWindowVM.fs
namespace xxxxLab.ViewModels

open System
open System.Reactive.Linq
open Reactive.Bindings

type MainWindowVM() =
  
  let initValue = 50
  let maxValue = 55
  let minValue = 45

  // ReactiveProperty Version
  let count = new ReactiveProperty<int>(initValue)
  let plusCommand = count.Select(fun p -> p < maxValue).ToReactiveCommand()
  let minusCommand = count.Select(fun p -> p > minValue).ToReactiveCommand()
  do plusCommand.Subscribe (fun _ -> count.Value<-count.Value+1) |> ignore
  do minusCommand.Subscribe (fun _ -> count.Value<-count.Value-1) |> ignore

  member x.WindowTitle = "F# MVVM ReactiveProperty ver."
  member x.Count = count
  member x.PlusCommand = plusCommand
  member x.MinusCommand = minusCommand 
```

## ViewModelの設計 SodiumFRP版

SoduimをReactiveProperty的に使えるように`SodiumRP`、ReactiveCommand的に使えるように`SodiumRC`を定義しました。

- Nugetで「SodiumFRP.FSharp 4.0.5」を追加

``` FSharp:SodiumRP.fs
namespace xxxLab.ViewModels

open System
open SodiumFRP
open System.ComponentModel
open System.Windows.Input

type SodiumRP<'T>( stateCell:Cell<'T> ) as this =

  let ev = new Event<_,_>()  
  let propChangedNotifier _ = postT <| fun _ -> ev.Trigger(this, PropertyChangedEventArgs("Value"))
  let listener:IStrongListener = stateCell 
                                 |> listenC propChangedNotifier 

  member x.Value = Cell.sample stateCell

  interface IDisposable with
    member __.Dispose() = listener.Unlisten ()

  interface INotifyPropertyChanged with
    [<CLIEvent>]
    member x.PropertyChanged = ev.Publish

type SodiumRC<'T>( commandExecuteStream :StreamSink<Option<'T>> , canExecuteCell:Cell<bool> ) as this =
  let ev = new Event<_,_>()
  let commandExecute p = StreamSink.send p commandExecuteStream
  let canExecuteChangedNotifier _ = postT <| fun _ -> ev.Trigger(this, EventArgs())
  let listener:IStrongListener = canExecuteCell |> listenC canExecuteChangedNotifier

  interface IDisposable with
    member __.Dispose() = listener.Unlisten ()

  interface ICommand with
    member this.CanExecute obj = Cell.sample canExecuteCell
    member this.Execute commandParameter = 
      let p = match commandParameter with
                 | null -> None
                 | x -> Some (x:?>'T)
      commandExecute p

    [<CLIEvent>]
    member this.CanExecuteChanged = ev.Publish
```

`SodiumRP`と`SodiumRC`を利用したViewModelが、次の`MainWindowVM.fs`のようになります。

### 概念図

- 各コマンドの実行（発火）をストリーム（`ssPlus`、`ssMinus`）としてモデル化しています。
- カウント値（状態）をセル（`cCount`）としてモデル化しています。
- 各コマンドが実行可能か？という状態をセル（`cCanExecutePlus`,`cCanExecuteMinus`）としてモデル化しています。 

![名称未設定-2.png](https://qiita-image-store.s3.amazonaws.com/0/152805/613bbd13-a7c6-6cc6-add6-b46d33831a3b.png)

### コード

``` FSharp:MainWindowVM.fs
namespace xxxxLab.ViewModels

open System
open SodiumFRP

type MainWindowVM() =
  
  let initValue = 50
  let maxValue = 55
  let minValue = 45

  // SodiumFRP Version
  let ssPlus = StreamSink.create<Option<unit>>()
  let ssMinus = StreamSink.create<Option<unit>>()
  let cCount = Sodium.loopWithNoCapturesC ( fun value ->
      let sPlusDelta = ssPlus |> Stream.map (fun _ -> 1 )
      let sMinusDelta = ssMinus |> Stream.map (fun _ -> -1)
      let sDelta = (sPlusDelta, sMinusDelta) |> Stream.orElse 
      let sCountUpdate = sDelta |> Stream.snapshot value (+)
      sCountUpdate |> Stream.hold initValue )

  let cCanExecutePlus = cCount |> mapC (fun p -> p < maxValue) 
  let cCanExecuteMinus = cCount |> mapC (fun p -> p > minValue) 

  member x.WindowTitle = "F# MVVM SodiumFRP ver."
  member x.PlusCommand = new SodiumRC<unit>(ssPlus,cCanExecutePlus) 
  member x.MinusCommand = new SodiumRC<unit>(ssMinus,cCanExecuteMinus) 
  member x.Count = new SodiumRP<int>(cCount)
```

# コマンドパラメータで増分値を指定

次のように「+3」ボタンを追加することを考えます。なお、カウント値が54のときに「+3」が押されても57ではなく55（カウント範囲の最大値）になるように動作させます。

![2019-02-06_12h17_28.png](https://qiita-image-store.s3.amazonaws.com/0/152805/aed88b02-157e-eb13-010d-ca8064978a19.png)

## Viewの修正変更

次のように、CommandParameterによりView上でstring型で増分値（この場合は「+3」）を与えるものとします。また、CommandParameterなしでCommandを実行した場合は、それぞれ「+1」「-1」の増分値が与えられるものとします。

```xml:MainWindow.xaml（変更部周辺を抜粋）
<TextBlock Text="{Binding Count.Value}"/>
<Button Content="+1" Command="{Binding PlusCommand}"/>
<Button Content="+3" Command="{Binding PlusCommand}" 
                     CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
<Button Content="-1" Command="{Binding MinusCommand}"/>
```

## ViewModelの修正変更 SodiumFRP版

主な変更点は次のようになります。

- string型を想定したコマンドパラメータを扱うために、`ssPlus` と `ssMinus` を、`StreamSink.create<Option<unit>>()` から`StreamSink.create<Option<string>>()` に変更しました。
- ストリーム `ssPlus` にはオプション型（`Option<string>`）のデータが流れます。具体的には、コマンドパラメータが指定されてるときは `Some("string型のコマンドパラメータ値")`、そうでないときは `None` となります。
 - データが `None` でなければ `Option.get` により内容を取り出しています。
- `Stream.snapshot value (fun d v -> Math.Min(d,maxValue-v))` により、カウント値が上限を超えないように調整しています。
  - とても分かりずらいですが、`d`がパイプ演算子により前から渡される増分値、`v`がスナップショットにより取得された現在のカウント値となります。 
  - ![無題8.png](https://qiita-image-store.s3.amazonaws.com/0/152805/863a368a-3834-1d26-0e97-89cbc372f096.png)


``` FSharp:MainWindowVM.fs
namespace xxxxLab.ViewModels

open System
open SodiumFRP
open System

type MainWindowVM() =
  
  let initValue = 50
  let maxValue = 55
  let minValue = 45

  // SodiumFRP Version
  let ssPlus = StreamSink.create<Option<string>>()
  let ssMinus = StreamSink.create<Option<string>>()
  let cCount = Sodium.loopWithNoCapturesC ( fun value ->
      let sPlusDelta = ssPlus 
                       |> Stream.map (fun p -> if p=None then 1 else int <| Option.get p )
                       |> Stream.snapshot value (fun d v -> Math.Min(d,maxValue-v)) 
      let sMinusDelta = ssMinus 
                        |> Stream.map (fun p -> if p=None then -1 else int <| Option.get p)
      let sDelta = (sPlusDelta, sMinusDelta) |> Stream.orElse 
      let sCountUpdate = sDelta |> Stream.snapshot value (+)
      sCountUpdate |> Stream.hold initValue )

  let cCanExecutePlus = cCount |> mapC (fun p -> p < maxValue) 
  let cCanExecuteMinus = cCount |> mapC (fun p -> p > minValue) 

  member x.WindowTitle = "F# MVVM SodiumFRP ver."
  member x.PlusCommand = new SodiumRC<string>(ssPlus,cCanExecutePlus) 
  member x.MinusCommand = new SodiumRC<string>(ssMinus,cCanExecuteMinus) 
  member x.Count = new SodiumRP<int>(cCount)
```

# 参考資料
- [F#+WPF+ReactiveProperty@Qiita](https://qiita.com/ozwk/items/cdf0072f4a04f158490e)
- [SodiumでFunctional Reactive Programming (F#)@何でもプログラミング](http://any-programming.hatenablog.com/entry/2017/01/28/162128)
- 実践F# 関数型プログラミング入門 (技術評論社)
- 関数型リアクティブプログラミング (翔泳社)