Haskell 狀態Monad (State Monad)的理解

1、State s a數據類型

State s a是一種用來封裝狀態處理函數:\s -> (a,s’)的數據結構,因爲State封裝的是一個函數,而不是狀態s本身,所以稱State爲State類型(type)是不準確的,應該把State s a稱爲狀態處理器(State processor)。

newtype State s a = State { runState :: s -> (a,s) }
-- 即:newtype State s a = State (s -> (a,s))
  • 注意】(\s -> (a,s’)) 是狀態處理函數,封裝了狀態處理函數的State容器叫做狀態處理器,不要混淆了。

  • 儘管將State稱爲類型不準確,但畢竟State s a是通過 newtype定義的,也就是對現有類型的封裝。我們可以不準確地將其稱爲State數據類型,該類型有兩個類型參數:表示狀態的類型參數 s 以及表示結果的類型參數 a。雖然寫作State s a,但在理解上,我們應該把State看成是一個裝了狀態處理函數的容器State (s -> (a, s)),即狀態處理器。

  • State s a的實質是State (s -> (a, s)),State s a與State (s -> (a, s))完全等價。

  • 通過 runState 訪問函數可以從 State 類型中取出這個封裝在裏面的狀態處理函數:\s -> (a,s’),該函數接收一個狀態參數 s,經過計算(轉換)之後返回一個由狀態s下對應的返回結果 a 以及新的狀態 s’的二元組(a, s’)。runState可以直接當作狀態處理函數的函數名,他相當於把State容器中的狀態處理函數從State容器中取出來。


2、State s的Monad實例

instance Monad (State s) where
return :: x -> State s x 
return x = state (\s -> (x, s))    --注意這裏state的s是小寫
-- return x = State $ \s -> (x,s)  --注意這裏的State的S是大寫

(>>=) :: State s a -> (a -> State s b) -> State s b
pr >>= f = state $ \ st ->
   let (x, st') = runState pr st   -- Running the first processor on st
   in runState (f x) st'           -- Running the second processor on st'
  • state :: (s -> (a, s)) -> State s a 即:把一個狀態處理函數封裝到State容器裏

  • return函數把結果x封裝成一個State s x即State (s -> (x, s))的狀態處理器

  • bind 函數的類型簽名爲:(>>=) :: State s a -> (a -> State s b) -> State s b
    大致相當於 State (s -> (a, s)) -> (a -> State (s -> (b, s))) -> State (s -> (b, s))

  • 函數(a -> State s b)相當於一個生成狀態處理器State (s -> (b, s))的函數,稱爲狀態處理器生成函數f。

  • 整個bind函數最後返回的結果爲一個狀態處理器State(s -> (b, s)),即用裝入了狀態處理函數的State容器

在這裏插入圖片描述

(1) 對bind函數(>>=)的理解:

  1. bind運算的左側爲一個狀態處理器pA (寫爲State s a類型,理解爲裝了一個狀態轉換函數的State容器State (\s1 -> (v1, s2)))

  2. bind運算的右側爲一個狀態處理器生成函數f (即: \v1 -> State (\s2 -> (v2, s3))),該函數接受一個結果v1,返回一個狀態處理器pB(即:State (\s2 -> (v2, s3)))

  3. 我們將pA >>= f 產生pB的這一個過程記爲pAB,即pAB = pA >>= f,pAB爲一個封裝了由s1到(v2, s3)的State容器

【注意】pB與pAB不是一樣的,pB是封裝了(\s2 -> (v2, s3)) 的State容器,而pAB是封裝了(\s1 -> (v2, s3)) 的State容器

(2) 數據流向:

  1. 首先給定一個初始狀態s1(常稱爲seed)和一個狀態處理器pA,在這個狀態s1下,我們通過runState把狀態處理器pA中的狀態處理函數取出來,並向該函數傳入狀態s1,得到一個包含狀態s1下對應的返回結果v1以及新狀態s2的二元組(v1,s2)

  2. 通過狀態處理器生成函數f,我們將得到的二元組(v1,s2)中的v1取出傳入f中,得到一個新的狀態處理器pB。之所以要把v1取出傳給函數f是因爲v1是State s這個Monad中State s a的a,而f需要接收一個State s a中的a,從而生成一個State s b(即pB)

  3. 通過runState函數將pB中的狀態處理函數取出來,傳入新的狀態s2,得到一個包含狀態s2下對應的返回結果v2以及新狀態s3的二元組(v2,s3)

(3) 做bind的意義

在計算 pA >> f 的過程中,我們產生了兩個不同狀態(s1、 s2)下對應的兩個結果(v1、v2),並將狀態更新爲了s3

這就像我們在做僞隨機投骰子一樣,得到了兩次投擲結果v1、v2,並將狀態跟新了,以便下次能繼續根據新狀態s3產生投擲結果

我們可以把狀態處理器pA、pB理解成骰子,狀態處理器生成函數 f 理解爲骰子生成器。相同狀態下骰子產生的結果是唯一的。

  • 首先我們有一個狀態s1和一個骰子pA,骰子pA根據狀態s1投出了結果v1,並且將狀態更新爲s2
  • 然後骰子生成器 f 接收到v1,得知骰子pA已經被使用,隨後生成一個新的骰子pB,新的骰子pB根據新的狀態s2投出了新的結果v2,並將狀態更新爲s3。

所以:
p >>= f 可以理解爲投了兩次骰子得到了兩個結果
p >>= f >>= f 可以理解爲投了三次骰子得到三個結果
以此類推…


3、State Monad中的相關函數

(1) 設置與讀取State

在前面,我們討論了給定一個初始狀態s和一個狀態處理器p,我們可以得到一個返回結果a和一個新的狀態s’。初始狀態的話我們可以直接設定,那麼初始狀態處理器是怎麼得到的呢呢?

通過put函數生成狀態處理器:

put newState = State $ \_ -> ((), newState)
  • put函數首先接收一個我們給定的初始狀態newState,然後生成一個狀態處理器State (_ -> ((), newState))。這個狀態處理器忽略它接收到的任何state參數,然後返回一個二元組,這個二元組裏包含一個空結果()和輸入給put函數的初始狀態newState。

  • 由於我們不需要這個初始狀態newState下返回的結果,我們只是需要一個狀態來替換,所以元組的第一個元素是(),一個佔位的值(universal placeholder value)。

(2) 獲取值與狀態

通過evalState函數,我們可以獲取狀態處理器p在傳入狀態s下,返回的結果a

evalState :: State s a -> s -> a
evalState p s = fst (runState p s)

通過execState函數,我們可以獲取狀態處理器p在傳入狀態s後,生成的新狀態s’

execState :: State s a -> s -> s
execState p s = snd (runState p s)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章