読者です 読者をやめる 読者になる 読者になる

F#でWPF --- Elm Architectureを利用したMVVM --- Model側のキャッシュ

Elm F# MVVM WPF

下記記事で紹介いたしました構造において、Viewの差分更新は自動で行われるのですが、その他の部分は全て毎回作成することになっています。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

もし時間のかかる処理が存在する場合は毎回計算するわけにはいきません。そこで簡単なキャッシュ機能を実装します。

作成するアプリ

カウンタとカウンタの値を2倍したテキスト、乱数を生成するボタンからなります。

カウンタと乱数生成ボタンは完全に独立したものとなっています。
f:id:any-programming:20170126202048p:plain

実装

とりあえず何も考えずに実装します。

コード内で利用しているDataContextクラスは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

Xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="130" Width="300">
    <Grid>
        <!-- カウンタ -->
        <Button Content="-" Command="{Binding Decrement}" HorizontalAlignment="Left" Margin="29,23,0,0" VerticalAlignment="Top" Width="25"/>
        <TextBlock Text="{Binding Count}" HorizontalAlignment="Left" Margin="80,25,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
        <Button Content="+" Command="{Binding Increment}" HorizontalAlignment="Left" Margin="115,23,0,0" VerticalAlignment="Top" Width="25"/>
        <Label Content="x 2 = " HorizontalAlignment="Left" Margin="167,20,0,0" VerticalAlignment="Top"/>
        <TextBlock Text="{Binding DoubleCount}" HorizontalAlignment="Left" Margin="233,25,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>

        <!-- 乱数生成 -->
        <Button Content="Random" Command="{Binding GenerateRandom}" HorizontalAlignment="Left" Margin="29,61,0,0" VerticalAlignment="Top" Width="75"/>
        <TextBlock Text="{Binding Random}" HorizontalAlignment="Left" Margin="128,63,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    </Grid>
</Window>

F#

open System
open System.Windows

type Msg =
    | Increment
    | Decrement
    | GenerateRandom

type Model = 
    { Count  : int
      Random : int }
    member this.DoubleCount = this.Count * 2

let random = Random()

let updateModel model msg =
    match msg with
    | Increment ->
        { model with Count = model.Count + 1 }

    | Decrement ->
        { model with Count = model.Count - 1 }

    | GenerateRandom ->
        { model with Random = random.Next() }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window

    window.DataContext <- DataContext({ Count = 0; Random = 0 }, updateModel, id)

    Application().Run(window) |> ignore    
    0


仮想的に処理を重くする

現状のままでは重い処理が存在しないので、Sleepにて仮想的に処理を重くします。

type Model = 
    { Count  : int
      Random : int }
    member this.DoubleCount =
        Threading.Thread.Sleep(1000)
        this.Count * 2


乱数生成まで遅くなる

毎回ViewModelを作成しているため(厳密にはDataContextクラス内にて差分を取る際に全プロパティのgetterが呼ばれるため)、乱数生成の処理まで重くなってしまっています。

Cache

mutable Option、input、input -> outputを受け取り、inputがキャッシュと同じならキャッシュを返し、それ以外は新たに処理をして返す&キャッシュに収納する関数を作成します。

module Cache =
    let get (cache : byref<_>) x f =
        match cache with
        | Some (_x, _y) when _x = x -> _y
        | _ -> let y = f x
               cache <- Some (x, y)
               y


Cache組み込み

type Model = 
    { Count  : int
      Random : int 
      mutable Cache_DoubleCount : (int * int) option }
    member this.DoubleCount = 
        (fun x -> 
            Threading.Thread.Sleep(1000)
            x * 2)
        |> Cache.get &this.Cache_DoubleCount this.Count 


乱数生成への影響がなくなる

Cache組み込み後は、乱数生成を行ってもカウンタの値が変更されていないので、DoubleCountの値は前回の結果が利用されます。

updateModelでの更新との違い

Cacheを利用しなくても、updateModelの段階で処理して代入しておけば同じ効果が得られます。

ただ入力の数が増えた場合に関連する全てのメッセージ内で代入処理をしなければならないため、Cacheを利用したほうが楽に管理できます。