動機付け
前回までで、モデルとメッセージの分離を行い、テストが容易なupdate
関数を切り出しました。
次はsetModel
関数のパフォーマンスについて考えます。
まずはsetModel
関数を再掲載します。
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}"
基本的にこの関数はモデルのCount
が変わってもStep
が変わっても、どちらにせよすべてのコントロールを再描画します。
それどころか、モデルが一切変わっていなかったとしてもsetModel
関数が呼ばれれば再描画が行われます。
モデルのStep
が変わったときはlabelCount
コントロールを更新する必要はありませんし、モデルのCount
が変わったときも同様です。
現時点のように小規模なアプリケーションではほとんど影響はありませんが、大規模になってくるとこのことが問題になってくるかもしれません。
というのも、差分検出よりも画面描画のほうが一般にコストが高いことが多いからです。
パフォーマンスが要求される場合、モデルの差分を計算し、必要な部分だけを再描画するようにしなければならないでしょう。
差分比較
まず、model
変数の型をModel
からModel option
に変更します。
そして、model
変数の初期値を最初はNone
とします。
- let mutable model = initialModel
+ let mutable model = None
次に、コンストラクタでモデルの初期状態initialModel
をセットします。
- do setModel model
+ do setModel initialModel
dispatch
関数に関しては今まで通りの挙動としますが、model
変数がまだ初期化されていない状態、すなわちNone
値の場合は呼び出されても何もしないこととします。
let dispatch msg =
model
|> Option.map (update msg)
|> Option.iter setModel
さて、次は本題のsetModel
関数です。これを書き直しましょう。
model
変数がNone
のときは明らかに画面の更新が必要です。
Some
のときはモデルの各フィールドに変更がある場合はそれに対応するコントロールのみを更新します。
let setModel nextModel =
// Count diff
match model with
| Some prevModel when prevModel.Count = nextModel.Count -> ()
| _ ->
this.labelCount.Text <- $"現在のカウント数は %d{nextModel.Count} です。"
// Step diff
match model with
| Some prevModel when prevModel.Step = nextModel.Step -> ()
| _ ->
this.numericUpDownStep.Value <- nextModel.Step |> decimal
this.buttonIncrement.Text <- $"+%d{nextModel.Step}"
this.buttonDecrement.Text <- $"-%d{nextModel.Step}"
model <- Some nextModel
最終的に次のようなコードになります。
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 = None
let setModel nextModel =
match model with
| Some prevModel when prevModel.Count = nextModel.Count -> ()
| _ ->
this.labelCount.Text <- $"現在のカウント数は %d{nextModel.Count} です。"
match model with
| Some prevModel when prevModel.Step = nextModel.Step -> ()
| _ ->
this.numericUpDownStep.Value <- nextModel.Step |> decimal
this.buttonIncrement.Text <- $"+%d{nextModel.Step}"
this.buttonDecrement.Text <- $"-%d{nextModel.Step}"
model <- Some nextModel
do setModel initialModel
let dispatch msg =
model
|> Option.map (update msg)
|> Option.iter setModel
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
なお、差分更新するまでもない小規模なアプリケーションでは、保守性の観点から次のようにsetModel
関数をなるべくシンプルに保っておいたほうが良いでしょう。
let setModel nextModel =
this.labelCount.Text <- $"現在のカウント数は %d{nextModel.Count} です。"
this.numericUpDownStep.Value <- nextModel.Step |> decimal
this.buttonIncrement.Text <- $"+%d{nextModel.Step}"
this.buttonDecrement.Text <- $"-%d{nextModel.Step}"
model <- Some nextModel
パフォーマンスに大きく影響するような処理があれば、その部分だけ個別に対応するなどの工夫があっても良いかもしれません。
アダプティブデータ
本書では使用しませんが、このような計算を行うためのライブラリとして、FSharp.Data.Adaptiveなどが存在するので参考にしてみてください。