目標
メッセージの抽出
前回まででアプリケーションのモデルを定義しました。
type Model =
{ Count: int
Step: int }
モデルが決まれば画面に表示するべき内容が定まります。
それでは、モデルを変更する機能を列挙してみましょう。
- カウントの増加
- カウントの減少
- ステップの変更
これをコードで明示的に表してみます。
そのために、判別共用体を用います。
type Msg =
| Increment
| Decrement
| SetStep of int
カウントの増加と減少に関してはモデルのCount
とStep
を使えば次の状態を計算できますが、ステップの変更に関してはステップ数をどの値にするのかという追加情報が必要です。
したがって、SetStep
にはof int
が必要です。
アプリケーションの状態を変更する方法は、今定義した型Msg
の値で表されます。
逆に、これ以外の方法でアプリケーションの状態は変更しないこととします。(つまりsetModel
関数を呼ばない)
さて、モデルの値とメッセージの値から次のモデルの値を計算する関数update
を作成しましょう。
判別共用体で定義したMsg
型の値で場合分けを行います。
let update msg model =
match msg with
| Increment ->
{ model with
Count = model.Count + model.Step }
| Decrement ->
{ model with
Count = model.Count - model.Step }
| SetStep nextStep ->
{ model with
Step = nextStep }
このような関数はリデューサー(reducer)と呼ばれることもあります。
update
関数が定義できたので、現在の状態(型Model
の値)とそれに対する変更(型Msg
の値)があれば、次の状態を計算することができます。
先ほど、メッセージを経由することでのみアプリケーションの状態は変更することにしたことを思い出してください。
つまり、update
関数とsetModel
関数はひとまとまりの処理として呼ばれることになります。
そこで、次のようにdispatch
関数を定義します。
これはメッセージを引数にとり、現在の状態から次の状態を計算してそれを画面に反映させる関数です。
let dispatch msg =
let nextModel = update msg model
setModel nextModel
イベント購読中にsetModel
を使っている箇所はすべてdispatch
関数を使うように書き換えます。
let subscriptions =
[| this.buttonIncrement.Click
|> Observable.subscribe (fun e -> dispatch Increment)
this.buttonDecrement.Click
|> Observable.subscribe (fun e -> dispatch Decrement)
this.numericUpDownStep.ValueChanged
|> Observable.subscribe (fun e -> dispatch (SetStep(this.numericUpDownStep.Value |> int))) |]
全体的には次のようなコードになります。
module MainForm
open App.Designer
type Model =
{ Count: int
Step: int }
type Msg =
| Increment
| Decrement
| SetStep of int
let initialModel =
{ Count = 42
Step = 1 }
let update msg model =
match msg with
| Increment ->
{ model with
Count = model.Count + model.Step }
| Decrement ->
{ model with
Count = model.Count - model.Step }
| SetStep nextStep ->
{ model with
Step = nextStep }
type View() as this =
inherit MainForm()
let mutable model = initialModel
let setModel nextModel =
model <- nextModel
this.labelCount.Text <- $"現在のカウント数は %d{model.Count} です。"
this.numericUpDownStep.Value <- model.Step |> decimal
this.buttonIncrement.Text <- $"+%d{model.Step}"
this.buttonDecrement.Text <- $"-%d{model.Step}"
do setModel model
let dispatch msg =
let nextModel = update msg model
setModel nextModel
let subscriptions =
[| this.buttonIncrement.Click
|> Observable.subscribe (fun e -> dispatch Increment)
this.buttonDecrement.Click
|> Observable.subscribe (fun e -> dispatch Decrement)
this.numericUpDownStep.ValueChanged
|> Observable.subscribe (fun e -> dispatch (SetStep(this.numericUpDownStep.Value |> int))) |]
override this.Dispose(disposing) =
if disposing then
subscriptions
|> Array.iter (fun subscription -> subscription.Dispose())
base.Dispose disposing
アプリケーションを表す最小の模型
さて、ここまででアプリケーションのモデルとメッセージを抽出し、画面から分離してきました。
これにどのような意味があるのでしょうか?
分離してきた部分のみを切り出して再掲載します。
type Model =
{ Count: int
Step: int }
type Msg =
| Increment
| Decrement
| SetStep of int
let initialModel =
{ Count = 42
Step = 1 }
let update msg model =
match msg with
| Increment ->
{ model with
Count = model.Count + model.Step }
| Decrement ->
{ model with
Count = model.Count - model.Step }
| SetStep nextStep ->
{ model with
Step = nextStep }
この部分を丸ごとコピーし、F#インタラクティブにペーストします。
そして、文末にセミコロンを2つ;;
つけてから実行します。
これにより、F#インタラクティブ中にこれらを使用することができます。
試しにF#インタラクティブ上でupdate
関数を実行してみましょう。
> update Increment initialModel;;
val it: Model = { Count = 43
Step = 1 }
初期状態にIncrement
メッセージによる変更を加えると、カウント数43
、ステップ数1
になります。
これは期待通りの挙動ですね。
次は、初期状態にSetStep 5
メッセージによる変更を加えた後、そのままIncrement
メッセージによる変更を加えてみましょう。
F#インタラクティブでは、it
という変数に前回評価した値が格納されています。
> update (SetStep 5) initialModel;;
val it: Model = { Count = 42
Step = 5 }
> update Increment it;;
val it: Model = { Count = 47
Step = 5 }
最終的にカウント数47
、ステップ数5
になりました。
これも期待通りの挙動ですね。
このように、モデルとメッセージを画面上のコントロールから切り離したことで、画面無しでアプリケーションの動きを確認することができます。
この性質により、アプリケーションのテストが比較的容易になります。
問題が発生した場合は以下の観点に切り分けができます。
- 意図したメッセージが
dispatch
されているか? -
update
関数による「次のモデル」の計算は正しいか? - モデルが
setModel
関数によって正しく表示されているか?
このうち、update
関数の挙動は簡単にテストすることができました。