不太明顯的是該類型的構造需要功能從一個狀態到修改後的狀態和結果,就像這樣:
newtype State stateData result = State (stateData -> (result, stateData))
因此,儘管單子被稱爲「國家」,包的實際價值由monad得到的是的一個基於狀態的計算,而不是包含狀態的實際值。
記住這一點,我們不應該驚訝地發現用於在State monad中執行計算的函數runState
實際上只不過是包裝函數本身的訪問器,並且可以像這樣定義:
runState (State f) = f
那麼定義一個返回狀態值的函數是什麼意思?讓我們暫時忽略一個事實,即國家是一個單子,只看基本類型。首先,考慮此功能(這實際上不符合國家做任何事情):
len2State :: String -> State Int Bool
len2State s = return ((length s) == 2)
如果你看一下國的定義中,我們可以看到,這裏的stateData
類型是Int
,而result
類型Bool
,因此由數據構造函數包裝的函數必須具有類型Int -> (Bool, Int)
。現在,想象一下無國家版本的len2State
--顯然,它會有String -> Bool
。那麼如何將這樣一個函數轉換成一個返回適合於狀態包裝的值呢?
很明顯,轉換函數需要第二個參數,代表狀態值的Int
。它還需要返回一個狀態值,另一個爲Int
。由於我們在這個函數中並沒有對狀態做任何事情,所以讓我們做一些明顯的事情 - 把這個int傳遞給它。這裏是一個狀態函數,按照無狀態版本來定義:
len2 :: String -> Bool
len2 s = ((length s) == 2)
len2State :: String -> (Int -> (Bool, Int))
len2State s i = (len2' s, i)
但是這樣做很愚蠢和多餘。讓我們概括一下轉換,以便我們可以傳遞結果值,並將任何東西變成類似狀態的函數。
convert :: Bool -> (Int -> (Bool, Int))
convert r d = (r, d)
len2 s = ((length s) == 2)
len2State :: String -> (Int -> (Bool, Int))
len2State s = convert (len2 s)
如果我們想要一個改變狀態的函數呢?顯然,我們不能用convert
建立一個,因爲我們寫了這個來通過狀態。讓我們保持簡單,寫一個函數來用新值覆蓋狀態。它需要什麼類型的?它需要一個Int
作爲新的狀態值,當然必須返回一個函數stateData -> (result, stateData)
,因爲這是我們的狀態包裝器所需要的。覆蓋狀態值在狀態計算之外並沒有真正的result
值,所以我們的結果將只是()
,這是在Haskell中表示「無值」的零元素元組。
overwriteState :: Int -> (Int -> ((), Int))
overwriteState newState _ = ((), newState)
很簡單!現在,我們實際上與該狀態數據做一些事情。讓我們從上面將len2State
改寫成更明智的東西:我們將比較字符串長度和當前狀態值。
lenState :: String -> (Int -> (Bool, Int))
lenState s i = ((length s) == i, i)
我們可以將它推廣到轉換器和無狀態函數嗎?就像我們之前做的那樣?不太容易。我們的len
函數需要將狀態作爲參數,但我們不希望它「知道」狀態。的確很尷尬。但是,我們可以編寫一個快速幫助函數來處理我們所有的事情:我們將給它一個需要使用狀態值的函數,並且它將傳遞值並將所有內容打包回狀態函數離開len
不明智。
useState :: (Int -> Bool) -> Int -> (Bool, Int)
useState f d = (f d, d)
len :: String -> Int -> Bool
len s i = (length s) == i
lenState :: String -> (Int -> (Bool, Int))
lenState s = useState (len s)
現在,棘手的部分 - 如果我們想要將這些函數串在一起呢?比方說,我們希望對字符串使用lenState
,如果結果爲false,則將狀態值加倍,然後再次檢查字符串,如果兩次檢查都返回true。我們擁有完成這項任務所需的所有部分,但全部寫出來將是一件痛苦的事情。我們可以創建一個函數來自動鏈接兩個函數,每個函數返回類似狀態的函數嗎?當然可以!我們只需要確定它有兩個參數:第一個函數返回的狀態函數和一個函數,該函數將前一個函數的result
類型作爲參數。讓我們來看看它是如何變成了:
chainStates :: (Int -> (result1, Int)) -> (result1 -> (Int -> (result2, Int))) -> (Int -> (result2, Int))
chainStates prev f d = let (r, d') = prev d
in f r d'
這一切正在做的是將第一狀態函數的一些狀態數據,然後將第二個函數的結果和修改後的狀態數據。很簡單,對吧?
現在,有趣的部分:在chainStates
和convert
之間,我們幾乎應該能夠將無狀態函數的任意組合轉換爲啓用狀態的函數!我們現在唯一需要的是替換useState
,它返回狀態數據作爲結果,以便chainStates可以將它傳遞給那些不知道我們正在使用的技巧的函數。另外,我們將使用lambdas接受前面函數的結果並給它們臨時名稱。好吧,讓我們做到這一點:
extractState :: Int -> (Int, Int)
extractState d = (d, d)
chained :: String -> (Int -> (Bool, Int))
chained str = chainStates extractState $ \state1 ->
let check1 = (len str state1) in
chainStates (overwriteState (
if check1
then state1
else state1 * 2)) $ \ _ ->
chainStates extractState $ \state2 ->
let check2 = (len str state2) in
convert (check1 || check2)
,並嘗試一下:
> chained "abcd" 2
(True, 4)
> chained "abcd" 3
(False, 6)
> chained "abcd" 4
(True, 4)
> chained "abcdef" 5
(False, 10)
當然,我們不能忘記,國家實際上是包裝了類似於國家的職能,並保持我們的單子遠離他們,所以我們建造的漂亮功能都不會幫助我們實現真正的功能。或者他們會?在一個令人震驚的扭曲,事實證明,真正的國家單子都提供相同的功能,以不同的名字:
runState (State s) = s
return r = State (convert r)
(>>=) s f = State (\d -> let (r, d') = (runState s) d in
runState (f r) d')
get = State extractState
put d = State (overwriteState d)
注意>> =幾乎是相同的chainStates,但有來定義它沒有什麼好辦法使用chainStates。因此,包裝的東西,我們可以使用真正國家重寫最後一個例子:
chained str = get >>= \state1 ->
let check1 = (len str state1) in
put (if check1
then state1 else state1 * 2) >>= \ _ ->
get >>= \state2 ->
let check2 = (len str state2) in
return (check1 || check2)
或者,所有蜜餞了相當於做記號:
chained str = do
state1 <- get
let check1 = len str state1
_ <- put (if check1 then state1 else state1 * 2)
state2 <- get
let check2 = (len str state2)
return (check1 || check2)
很高興能讀到 – barkmadley 2009-12-24 09:10:40
謝謝,儘管它可能已經在點上使用了更多的作品,但也很高興寫出來。我擔心人們可能會被推遲,但我想不是...... – 2009-12-25 01:00:16