Skip to content

Latest commit

 

History

History
160 lines (120 loc) · 8.79 KB

07_implementingTheStateMonad.rst

File metadata and controls

160 lines (120 loc) · 8.79 KB

Stateモナドの実装

興味深いことに、通常のStateモナドはUpdateモナドの特別系 ではありません 。しかし、同様の機能 - 読み書き可能な状態を伴った計算 - を実装した計算を定義することはできます。

状態と更新

この最後の例では、 状態 を表す型 State更新 を表現する型 Update 両方が、意味のある役割を持つことになります。それらの型を自身が保持している値に対してジェネリックにします。 State は単に含んでいる値(現在の状態)のラッパーです。 Update は2種類 - 空の更新(何もしない)と状態を設定する更新 - を取り得ます。

/// 型'Tの状態をラップします
type StateState<'T> = State of 'T

/// 型'Tの状態に対する更新を表します
type StateUpdate<'T> =
  | Set of 'T | SetNop
  /// 空の更新 - 何も状態を変更しません
  static member Unit = SetNop
  /// 更新の結合 - 最新の(最も右にある) 'Set'更新を返します
  static member Combine(a, b) =
    match a, b with
    | SetNop, v | v, SetNop -> v
    | Set a, Set b -> Set b
  /// 状態に対して更新を適用します - 'Set'更新が状態を変更します
  static member Apply(s, p) =
    match p with SetNop -> s | Set s -> State s

この定義は前の2つに比べるとより興味深いものとなっています、なぜなら 状態更新 の間にいくつかの相互作用があるからです。特に、更新が Set v (現在の状態を新しいもので置き換えようとします) の時には、 Apply メンバは元とは異なる状態を返します。 Unit メンバについては、 単に元の状態を保持しておくという SetNop の更新が必要になります。(ですのでこの場合は Apply はただ単に元の値を返します。)

もう一つの特筆すべき点は、 Combine 操作 - 2つの更新(両方とも空の更新かも知れませんし通常の更新かもしれません)を受け取り一つの更新を返します - です。合成 a1 ++ a2 ++ .. ++ an を状態更新のシーケンス( Set でも SetNope でも構いません )として読み取った場合、 Combine 操作はシーケンス中の最後の Set 更新(一つも Set 更新がない場合は SetNop )を返します。言い換えると、シーケンス全体の中での最後の状態を設定するような更新を構築するのです。

State モナドプリミティブ

さあ、型の定義はできましたので、通常のプリミティブを追加するのはかなり簡単になります。

/// 指定された値に状態を設定する
let set s = UM (fun _ -> (Set s,()))
/// 現在の状態を取得する
let get = UM (fun (State s) -> (SetNop, s))
/// 初期状態を設定して計算を実行する
let setRun s (UM f) = f (State s) |> snd

set 操作は一般的なStateモナドのそれとは少し違います。状態を無視し、新しい状態を設定するための計算を表す 更新 を構築します。 get オペレーションは状態を読み取ってそれを返します - ただし何も変更しない場合には、更新として SetNop を返します。

状態を持った計算のサンプル

ここまで読んできた方なら、次の例がどんな風になるのか予想できるでしょう!もう一度 update { .. } コンピュテーション式を使います。今回は、 demo5 という、状態をインクリメントし demo6 のループの中から呼ぶコンピュテーション式を定義します。

/// 状態を1ずつインクリメントする
let demo5 = update {
  let! v = get
  do! set (v + 1) }
/// demo5をループの中で反復して呼び
/// 最後の状態を返す
let demo6 = update {
  for i in 1 .. 10 do
    do! demo5
  return! get }
// サンプルを初期状態 0 で実行させる
demo6 |> setRun 0

コードを実行すると、予想通り10という結果を得ます - ゼロから始まり、その状態を10回インクリメントしたわけです。 我々は UpdateBuilder の定義を拡張しましたので(前の章で行いました), いくつかの素敵な特典にタダ乗りで来ています - for ループを使うことや、( demo5 のように)ただ状態を変更したいだけの際には、明示的に return を書かなくても計算を記述することができるのです。