Chapter 07

アプリケーションメッセージの抽出

eagle
eagle
2021.12.26に更新

目標

メッセージの抽出

前回まででアプリケーションのモデルを定義しました。

type Model =
    { Count: int
      Step: int }

モデルが決まれば画面に表示するべき内容が定まります。

それでは、モデルを変更する機能を列挙してみましょう。

  • カウントの増加
  • カウントの減少
  • ステップの変更

これをコードで明示的に表してみます。
そのために、判別共用体を用います。

type Msg =
    | Increment
    | Decrement
    | SetStep of int

カウントの増加と減少に関してはモデルのCountStepを使えば次の状態を計算できますが、ステップの変更に関してはステップ数をどの値にするのかという追加情報が必要です。
したがって、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))) |]

全体的には次のようなコードになります。

MainForm.fs
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になりました。
これも期待通りの挙動ですね。

このように、モデルとメッセージを画面上のコントロールから切り離したことで、画面無しでアプリケーションの動きを確認することができます。
この性質により、アプリケーションのテストが比較的容易になります。

問題が発生した場合は以下の観点に切り分けができます。

  1. 意図したメッセージがdispatchされているか?
  2. update関数による「次のモデル」の計算は正しいか?
  3. モデルがsetModel関数によって正しく表示されているか?

このうち、update関数の挙動は簡単にテストすることができました。