本文為翻譯,個(gè)人學(xué)習(xí)之用,原地址
程序狀態(tài)
如果你以前有其他語言的編程經(jīng)驗(yàn)塔沃,你可能寫過一些函數(shù)或者方法來控制程序的狀態(tài)。概念化的話阳谍,狀態(tài)就是當(dāng)執(zhí)行一些計(jì)算過程的時(shí)候蛀柴,所必須的一個(gè)或者多個(gè)變量螃概,而且這些變量跟函數(shù)的參數(shù)沒有關(guān)聯(lián)。面向?qū)ο蟮恼Z言鸽疾,比如C++吊洼,就以成員變量的形式廣泛地使用了狀態(tài)變量。C語言中制肮,我們在函數(shù)的作用域外聲明變量來保持狀態(tài)冒窍。
在Haskell中,這種技術(shù)就沒那么容易直接應(yīng)用了豺鼻。因?yàn)楸仨氁褂米兞孔垡海馕吨瘮?shù)會隱藏掉一些依賴。這與Haskell的純函數(shù)(參數(shù)相同儒飒,函數(shù)每次調(diào)用結(jié)果一致)相違背谬莹。
幸運(yùn)的是,在大多數(shù)情況下桩了,我們可以避免這種額外的計(jì)算附帽,然后使用純函數(shù)的方式來跟蹤程序狀態(tài)。我們通過傳遞狀態(tài)信息來做到這點(diǎn)井誉,這樣那些隱藏的依賴就會變得明確了士葫。
State
類型就是一個(gè)精心設(shè)計(jì),讓這個(gè)過程更加便利的工具送悔。在這章中,我們會從一個(gè)典型的問題(產(chǎn)生偽隨機(jī)數(shù))中引進(jìn)state爪模,來展示它是怎樣幫助我們的欠啤。
偽隨機(jī)數(shù)
生成真正的隨機(jī)數(shù)是很困難的。計(jì)算機(jī)程序大多數(shù)情況下會使用偽隨機(jī)數(shù)來代替屋灌。之所以稱之為“偽”洁段,是因?yàn)樗鼈儾⒉皇钦嬲碾S機(jī)。它們使用算法(偽隨機(jī)數(shù)生成器)生成共郭,這種算法需要一個(gè)初始狀態(tài)(通常叫做種子)祠丝,然后根據(jù)這個(gè)狀態(tài)來產(chǎn)生一系列貌似隨機(jī)的數(shù)字。每次產(chǎn)生隨機(jī)數(shù)的時(shí)候除嘹,這個(gè)狀態(tài)就必須要更新写半,偽隨機(jī)數(shù)生成器就會刷新,如果知道了初始種子和生成算法尉咕,不同的偽隨機(jī)數(shù)序列是可以重現(xiàn)的叠蝇。
在Haskell中的實(shí)現(xiàn)
在大多數(shù)編程語言中,生成偽隨機(jī)數(shù)非常簡單:在庫中就已經(jīng)提供了生成偽隨機(jī)數(shù)的函數(shù)年缎。(甚至是真正的隨機(jī)數(shù)悔捶,取決于其實(shí)現(xiàn))铃慷。Haskell也不例外,在random
包中的System.Random
模塊中也有這個(gè)方法:
GHCi> :m System.Random
GHCi> :t randomIO
randomIO :: Random a => IO a
GHCi> randomIO
-1557093684
GHCi> randomIO
1342278538
randomIO
是一個(gè)IO
action蜕该。它也不例外犁柜,使用一個(gè)可變的狀態(tài)。因?yàn)檫@個(gè)隱藏的依賴堂淡,這個(gè)函數(shù)生成的偽隨機(jī)數(shù)每次都是不同的馋缅。
例子:擲骰子
假定我們在寫一個(gè)擲骰子的游戲,我們創(chuàng)建一個(gè)擲骰子的方法淤齐。我們使用IO
函數(shù)randomRIO
股囊,它允許我們指定一個(gè)隨機(jī)數(shù)的范圍。對于六面篩子更啄,randomRIO (1, 6)
.
import Control.Monad
import System.Random
rollDiceIO :: IO (Int, Int)
rollDiceIO = liftM2 (,) (randomRIO (1,6)) (randomRIO (1,6))
這個(gè)方法會擲骰子兩次稚疹,(,)
是個(gè)函數(shù),接受兩個(gè)參數(shù)祭务,返回一個(gè)二維元組内狗,liftM2
讓(,)
函數(shù)能夠接受monadic參數(shù)。這樣义锥,這個(gè)函數(shù)會返回一個(gè)IO
元組柳沙。
練習(xí)
實(shí)現(xiàn)一個(gè)函數(shù)rollNDiceIO :: Int -> IO [Int]
,需要一個(gè)整型數(shù)(擲骰子次數(shù))拌倍,返回一個(gè)隨機(jī)數(shù)列表赂鲤,范圍1-6
去掉IO
randomRIO
一個(gè)缺點(diǎn)是,它必須使用IO
柱恤,將我們的狀態(tài)保存在程序外数初,我們并不能控制它。我們盡量只在必須要與外部世界交互的時(shí)候使用I/O梗顺。
為了避免使用IO
泡孩,我們創(chuàng)建一個(gè)本地的生成器。在System.Random
包中的 random
和mkStdGen
函數(shù)允許我們生成一個(gè)元組寺谤,其中包含偽隨機(jī)數(shù)和一個(gè)更新過的生成器仑鸥,以備下次這個(gè)函數(shù)下次調(diào)用。
GHCi> :m System.Random
GHCi> let generator = mkStdGen 0 -- "0" is our seed
GHCi> :t generator
generator :: StdGen
GHCi> generator
1 1
GHCi> :t random
random :: (RandomGen g, Random a) => g -> (a, g)
GHCi> random generator :: (Int, StdGen)
(2092838931,1601120196 1655838864)
注意
在random generator :: (Int, StdGen)
中变屁,我們使用::
來引進(jìn)類型注解眼俊,本質(zhì)上就是一個(gè)類型簽名。我們可以認(rèn)為random generator
是(Int, StdGen)
類型粟关。random
可以產(chǎn)生不同類型的值泵琳,如果我們想要Int
類型,我們最好通過類型簽名指定。
我們設(shè)法避免IO
获列,但這里又有一個(gè)新問題谷市。首先,如果我們要使用generator
獲取一個(gè)隨機(jī)數(shù)击孩,明顯我們定義...
GHCi> let randInt = fst . random $ generator :: Int
GHCi> randInt
2092838931
...是毫無用處的迫悠。它總是返回相同的值,2092838931
巩梢,因?yàn)槊看味际窍嗤纳善髟谙嗤臓顟B(tài)创泄。為了解決這個(gè)問題,我們可以使用元組的第二個(gè)成員(就是那個(gè)生成器)括蝠,然后傳遞給新調(diào)用的random
:
GHCi> let (randInt, generator') = random generator :: (Int, StdGen)
GHCi> randInt -- Same value
2092838931
GHCi> random generator' :: (Int, StdGen) -- Using new generator' returned from “random generator”
(-2143208520,439883729 1872071452)
這樣看起來好笨鞠抑,而且相當(dāng)啰嗦。
不使用IO
的擲骰子
我們使用一種新方法來擲骰子忌警,randomR
函數(shù):
GHCi> randomR (1,6) (mkStdGen 0)
(6, 40014 40692)
結(jié)果包含了一次擲骰子的結(jié)果和一個(gè)新的生成器搁拙。擲兩次骰子的實(shí)現(xiàn):
clumsyRollDice :: (Int, Int)
clumsyRollDice = (n, m)
where
(n, g) = randomR (1,6) (mkStdGen 0)
(m, _) = randomR (1,6) g
練習(xí)
實(shí)現(xiàn)rollDice :: StdGen -> ((Int, Int), StdGen)
函數(shù),接受一個(gè)生成器法绵,返回一個(gè)包含兩個(gè)隨機(jī)數(shù)的元組和一個(gè)生成器箕速。
clumsyRollDice
執(zhí)行一次就能得到我們想要的結(jié)果,但是朋譬,我們必須手動傳入生成器g
盐茎。它會隨著我們的程序的復(fù)雜變得越來越笨重。而且非常容易出錯(cuò):如果我們將中間的生成器傳入到一個(gè)錯(cuò)誤的where
從句中了呢徙赢?
我們真正需要的是一種能自動地提取元組的第二個(gè)參數(shù)字柠,然后傳遞給新一次的random
調(diào)用中。所以State
來了狡赐。
State 介紹
注意
在本章中募谎,我們使用Control.Monad.Trans.State
模塊transformers
包中的state monad。通過廣泛閱讀Haskell代碼阴汇,你會遇到Control.Monad.State
,一個(gè)和mtl
包密切相關(guān)的模塊节槐。這兩個(gè)模塊之間的區(qū)別現(xiàn)在可以不用關(guān)心:我們討論的都是應(yīng)用在mtl
的變種搀庶。
Haskell類型State
描述一個(gè)函數(shù),這個(gè)函數(shù)消費(fèi)一個(gè)state铜异,產(chǎn)出一個(gè)二維元組哥倔,包含結(jié)果和一個(gè)更新過的state。
這個(gè)狀態(tài)函數(shù)封裝了一個(gè)定義了runState
訪問器的數(shù)據(jù)類型揍庄。這樣就不用使用模式匹配了咆蒿。對于當(dāng)前目的,這個(gè)State
類型應(yīng)該定義成:
newtype State s a = State { runState :: s -> (a, s) }
這里,s
是state的類型沃测,a
是產(chǎn)出結(jié)果的類型缭黔。把它叫做State
類型可能有些不恰當(dāng),因?yàn)榉庋b的值并不是state本身蒂破,而是一個(gè)state處理器馏谨。
newtype
注意,我們使用newtype
關(guān)鍵字定義這個(gè)數(shù)據(jù)類型附迷,而不是data
惧互。newtype
只能有一個(gè)數(shù)據(jù)構(gòu)造器和一個(gè)字段。這樣保證了可以使用編譯器來做封裝和解封這個(gè)單個(gè)字段的工作喇伯。出于這個(gè)原因喊儡,我們通常使用newtype
來定義State
。使用type
來定義一個(gè)別名能否滿足需求呢稻据?答案是否定的艾猜,因?yàn)?code>type不允許我們?yōu)橐粋€(gè)新數(shù)據(jù)類型定義實(shí)例,這與我們的目的背道而馳攀甚。
State
構(gòu)造器在哪箩朴?
當(dāng)你開始使用Control.Monad.Trans.State
,你很快會注意到并沒有可用的State
構(gòu)造器秋度。這就是我們在前幾段介紹這個(gè)類型時(shí)炸庞,“我們當(dāng)前目的” 警告的原因。transformers
包以一種不大相同的方式實(shí)現(xiàn)了State
類型荚斯。這些差別并不影響我們使用和理解State
埠居;除了這個(gè),Control.Monad.Trans.State
導(dǎo)出了state
函數(shù)事期,來替代State
構(gòu)造器滥壕,做了同樣的工作。
state :: (s -> (a, s)) -> State s a
至于實(shí)現(xiàn)為什么不顯而易見兽泣,我們接下來介紹绎橘。
實(shí)例化Monad
到目前為止,我們所做的僅僅是包裝了一個(gè)方法唠倦,然后給它命名称鳞。還有另外一個(gè)任務(wù),State
是一個(gè)monad稠鼻,提供給我們很便利的方式去使用它冈止。不像我們以前見過的Functor
和Monad
實(shí)例,State
有兩個(gè)類型參數(shù)候齿。因?yàn)轭愋皖愔辉试S一個(gè)類型參數(shù)熙暴,因此我們要指出另外一個(gè)闺属,s
。
instance Monad (State s) where
這意味著有很多不同的State
monad周霉,每種類型都有可能成為state- State String
掂器,State Int
,State SomeLargeDataStructure
诗眨,等等唉匾。當(dāng)然,我們需要實(shí)現(xiàn)return
和(>>=)
方法匠楚;這些方法能夠處理所有s
取值的情況巍膘。
return
函數(shù)實(shí)現(xiàn):
return :: a -> State s a
return x = state ( \ st -> (x, st) )
給定一個(gè)值 (x
)到return
得到一個(gè)函數(shù),這個(gè)函數(shù)接受一個(gè)state (st
)芋簿,并將其和一個(gè)我們想要返回的值一起返回峡懈。最后一步,這個(gè)函數(shù)被一個(gè)state
函數(shù)包裹起來与斤。
綁定函數(shù)有一些繞:
(>>=) :: State s a -> (a -> State s b) -> State s b
pr >>= k = state $ \ st ->
let (x, st') = runState pr st -- Running the first processor on st.
in runState (k x) st' -- Running the second processor on st'.
(>>=)
需要兩個(gè)參數(shù)肪康,一個(gè)state處理器和一個(gè)根據(jù)第一個(gè)結(jié)果創(chuàng)建出另外一個(gè)處理器的函數(shù)k
。兩個(gè)處理器在一個(gè)接受初始化state (st
)并返回第二個(gè)結(jié)果和第三個(gè)state的函數(shù)中結(jié)合(這句話可能有問題)撩穿,總之磷支,(>>=
)允許我們依次運(yùn)行兩個(gè)state 處理器,第一階段的結(jié)果會影響到第二個(gè)階段的結(jié)果食寡。
一個(gè)實(shí)現(xiàn)的細(xì)節(jié)是runState
在State
的包裹下是怎樣使用的雾狈,我們深入到這個(gè)應(yīng)用到state上的函數(shù)中去。runState pr
抵皱,是s -> (a, s)
的實(shí)例善榛。
設(shè)置和訪問State
monad實(shí)例讓我們能夠操作各種state 處理器,但是在此時(shí)呻畸,你可能很好奇移盆,那個(gè)最原始的state是來自哪的碾牌。這個(gè)問題是由put
函數(shù)處理:
put newState = state $ \_ -> ((), newState)
給定一個(gè)state(我們想要引進(jìn)的那個(gè))仪吧,put
生成了一個(gè)state處理器,忽略它接收到的任何state干发,然后發(fā)揮返回提供給put
的state绞愚。由于我們不關(guān)心這個(gè)處理器的結(jié)果(我們要做的是替換這個(gè)state)叙甸,元組的第一個(gè)元素會是()
,一個(gè)占位的值爽醋。
get = state $ \st -> (st, st)
結(jié)果state處理器返回state st
,會被作為一個(gè)結(jié)果同時(shí)也作為一個(gè)state返回便脊。這就意味著這個(gè)state將不會改變蚂四,并且提供一個(gè)副本供我們操作。
獲取值和State
我們已經(jīng)實(shí)現(xiàn)了(>>=)
,runState
解包State a b
遂赠,來獲取真正的state處理函數(shù)久妆,這個(gè)函數(shù)接著會應(yīng)用于一些初始state。還有其他相似用途的函數(shù)比如evalState
和execState
跷睦。提供一個(gè)State a b
和一個(gè)初始state筷弦,evalState
會只返回state處理后的結(jié)果,execState
只返回新的state抑诸。
evalState :: State s a -> s -> a
evalState pr st = fst (runState pr st)
execState :: State s a -> s -> s
execState pr st = snd (runState pr st)
擲骰子和state
是時(shí)候?qū)?code>State monad應(yīng)用到我們擲骰子的例子上了烂琴。
import Control.Monad.Trans.State
import System.Random
我們希望通過類型StdGen的偽隨機(jī)發(fā)生器產(chǎn)生擲骰子的結(jié)果。因此state 處理器的類型是State StdGen Int
蜕乡,跟包裝后的StdGen -> (Int, StdGen)
一致奸绷。
現(xiàn)在我們可以實(shí)現(xiàn)的處理器,給定一個(gè)StdGen發(fā)生器层玲,產(chǎn)生1和6之間的一個(gè)數(shù)号醉。randomR
的類型是:
-- The StdGen type we are using is an instance of RandomGen.
randomR :: (Random a, RandomGen g) => (a, a) -> g -> (a, g)
看起來熟悉嗎?如果我們把a
看成Int
辛块,g
看成StdGen
畔派,就會變成:
randomR (1, 6) :: StdGen -> (Int, StdGen)
我們已經(jīng)有了一個(gè)state處理函數(shù)!現(xiàn)在缺少的是把它包裝進(jìn)state
:
rollDie :: State StdGen Int
rollDie = state $ randomR (1, 6)
出于便于理解的目的润绵,我們可以使用get
线椰,put
和do語法來寫rollDie
,這樣就能很清晰的展示出每一步的處理過程:
rollDie :: State StdGen Int
rollDie = do generator <- get
let (value, newGenerator) = randomR (1,6) generator
put newGenerator
return value
我們來過一遍每一個(gè)步驟:
- 第一步授药,我們使用
<-
士嚎,從一個(gè)monadic 上下文中取出偽隨機(jī)數(shù)生成器,供后邊使用悔叽。 - 然后莱衩,我們使用
randomR
函數(shù)和上步產(chǎn)生的生成器產(chǎn)生一個(gè)1-6的整數(shù)。我們把randomR
返回的新生成的生成器存儲起來娇澎。 - 我們接下來使用
put
把新的newGenerator
設(shè)置到state笨蚁,以至于以后的randomR
或者(>>=)
鏈能使用一個(gè)不同的隨機(jī)數(shù)生成器。 - 最后趟庄,我們使用
return
將結(jié)果注入到State StdGen
monad中括细。
最終我們可以使用我們的monadic骰子了。在此之前戚啥,初始state生成器本身是由mkStdGen函數(shù)生成奋单。
GHCi> evalState rollDie (mkStdGen 0)
6
為什么我們要把monad摻和進(jìn)來,創(chuàng)建了一個(gè)錯(cuò)綜復(fù)雜的框架僅僅是為了實(shí)現(xiàn)這個(gè)fst $ randomR (1,6)
猫十?好吧览濒,思考下以下函數(shù):
rollDice :: State StdGen (Int, Int)
rollDice = liftM2 (,) rollDie rollDie
我們得到一個(gè)可以產(chǎn)生一個(gè)包含兩個(gè)偽隨機(jī)數(shù)的二維元組的函數(shù)呆盖。注意一下他們的與眾不同之處:
GHCi> evalState rollDice (mkStdGen 666)
(6,1)
在其內(nèi)部,state是通過(>>=)
從一個(gè)rollDie
傳遞給其他的贷笛。而我們原本使用randomR (1, 6)
的做法十分笨重应又,因?yàn)槲覀儽仨毷謩拥膫鬟fstate。現(xiàn)在乏苦,monad實(shí)例幫助我們做這些事情株扛。假定我們知道怎樣使用lifting函數(shù),構(gòu)造復(fù)雜的隨機(jī)數(shù)組合(元組汇荐,列表或者其他)會突然變得很簡單洞就。

不同類型的隨機(jī)數(shù)
到目前為止,我們僅僅通過偽隨機(jī)數(shù)生成器產(chǎn)出了Int
類型的值拢驾。但是從randomR
的類型并不僅限于Int
奖磁。它可以生成System.Random
中Random
類中的任何類型的值。已經(jīng)實(shí)現(xiàn)了Int
繁疤,Char
咖为,Integer
,Bool
稠腊,Double
和Float
躁染,所以你可以生成它們其中的任何一個(gè)。
因?yàn)?code>State StdGen并不知曉關(guān)于生成的偽隨機(jī)數(shù)的類型架忌,所以我們可以寫一個(gè)相似的函數(shù)來提供一個(gè)并不指定類型的偽隨機(jī)數(shù):
getRandom :: Random a => State StdGen a
getRandom = state random
與rollDie
相比吞彤,這個(gè)函數(shù)在聲明中并不指定Int
類型,然后使用random
代替randomR
叹放;否則的話它們是相同的饰恕。getRandom
可以用在任何Random
實(shí)例上。
GHCi> evalState getRandom (mkStdGen 0) :: Bool
True
GHCi> evalState getRandom (mkStdGen 0) :: Char
'\64685'
GHCi> evalState getRandom (mkStdGen 0) :: Double
0.9872770354820595
GHCi> evalState getRandom (mkStdGen 0) :: Integer
2092838931
someTypes :: State StdGen (Int, Float, Char)
someTypes = liftM3 (,,) getRandom getRandom getRandom
allTypes :: State StdGen (Int, Float, Char, Integer, Double, Bool, Int)
allTypes = liftM (,,,,,,) getRandom
`ap` getRandom
`ap` getRandom
`ap` getRandom
`ap` getRandom
`ap` getRandom
`ap` getRandom
對于allTypes
井仰,因?yàn)闆]有liftM7
(標(biāo)準(zhǔn)庫中只到liftM5
)我們使用Control.Monad
中的 ap
函數(shù)替代埋嵌。ap
適合多次計(jì)算成多參數(shù)函數(shù)的應(yīng)用。理解ap
函數(shù)俱恶,看一下它聲明:
ap :: (Monad m) => m (a -> b) -> m a -> m b
謹(jǐn)記雹嗦,在Haskell中,a
類型變量可以被函數(shù)類型替換合是,跟這個(gè)比較:
GHCi>:t liftM (,,,,,,) getRandom
liftM (,,,,,) getRandom :: (Random a1) =>
State StdGen (b -> c -> d -> e -> f -> g
-> (a1, b, c, d, e, f, g))
很明顯了罪,monad m
變成了State StdGen
,ap
的第一個(gè)參數(shù)是一個(gè)函數(shù)b -> c -> d -> e -> f -> g -> (a1, b, c, d, e, f, g)
聪全。重復(fù)應(yīng)用ap
泊藕,我們最后得到一個(gè)元組,而不是一個(gè)函數(shù)难礼。把它們加起來娃圆,ap
把一個(gè)在monad中的函數(shù)應(yīng)用于一個(gè)monadic值(而liftM/fmap
汽久,是把一個(gè)不在monad中的函數(shù)應(yīng)用于一個(gè)monadic值)。
GHCi> evalState allTypes (mkStdGen 0)
GHCi>(2092838931,9.953678e-4,'\825586',-868192881,0.4188001483955421,False,316817438)