簡體   English   中英

State Monad,隨機數序列和monadic代碼

[英]State Monad, sequences of random numbers and monadic code

我正在努力掌握State Monad並且為了這個目的,我想編寫一個使用線性同余生成器生成一系列隨機數的monadic代碼(可能不好,但我的目的只是為了學習State Monad,而不是建立一個好的RNG庫)。

生成器就是這樣(為了簡單起見,我想生成一系列Bool ):

type Seed = Int

random :: Seed -> (Bool, Seed)
random seed = let (a, c, m) = (1664525, 1013904223, 2^32)  -- some params for the LCG
                  seed' = (a*seed + c) `mod` m
              in  (even seed', seed')   -- return True/False if seed' is even/odd 

不要擔心數字,這只是種子的更新規則(根據Numerical Recipes)應該生成Int s的偽隨機序列。 現在,如果我想按順序生成隨機數,我會這樣做:

rand3Bools :: Seed -> ([Bool], Seed)
rand3Bools seed0  = let (b1, seed1) = random seed0
                        (b2, seed2) = random seed1
                        (b3, seed3) = random seed2
                    in  ([b1,b2,b3], seed3)

好的,所以我可以通過使用State Monad避免這個樣板:

import Control.Monad.State

data Random {seed :: Seed, value :: Bool}

nextVal = do 
   Random seed val <- get 
   let seed' = updateSeed seed
       val'  = even seed'
   put (Random seed' val')
   return val'

updateSeed seed = let (a,b,m) = (1664525, 1013904223, 2^32) in (a*seed + c) `mod` m

最后:

getNRandSt n = replicateM n nextVal 

getNRand :: Int -> Seed -> [Bool]
getNRand   n seed = evalState (getNRandStates n) (Random seed True)

好的,這個工作正常,給我一個每個給定種子的n個偽隨機Bool的列表。 但...

我可以閱讀我所做的事情(主要基於這個例子: http//www.haskell.org/pipermail/beginners/2008-September/000275.html )並復制它以做其他事情。 但是我認為我不能理解do-notation和monadic函數背后的真實情況(比如replicateM)。

有人可以幫我解決一些疑惑嗎?

1 - 我試圖去掉nextVal函數來理解它的作用,但我不能。 我可以猜測它會提取當前狀態,更新它然后將狀態提前傳遞到下一個計算,但這只是基於讀取這個do-sugar,就像它是英語一樣。

我如何真正地將這個函數去掉原始的>> =並逐步返回函數?

2 - 我無法理解putget函數究竟是做什么的。 我猜他們會“打包”並“打開”狀態。 但是糖的背后的機制對我來說仍然是難以捉摸的。

好吧,關於這段代碼的任何其他一般性評論都是非常受歡迎的。 我有時會遇到Haskell,我可以創建一個有效的代碼並按照我的期望去做,但我不能“按照評估”,因為我習慣於使用命令式程序。

國家monad一開始看起來確實令人困惑; 讓我們按照Norman Ramsey的建議去做,並從頭開始實現如何實施。 警告,這很長!

首先,State有兩個類型參數: 包含的狀態數據的類型和計算最終結果的類型。 我們將在這里分別使用stateDataresult作為它們的類型變量。 如果你考慮一下,這是有道理的; 基於狀態的計算的定義特征是它在產生輸出時修改狀態。

不太明顯的是,類型構造函數從狀態到修改狀態和結果采用函數 ,如下所示:

newtype State stateData result = State (stateData -> (result, stateData))

因此,當monad被稱為“狀態”時,monad包含的實際值是基於狀態的計算 ,而不是包含狀態的實際值。

記住這一點,我們不應該驚訝地發現用於在State monad中執行計算的函數runState實際上只不過是包裝函數本身的訪問器,並且可以像這樣定義:

runState (State f) = f

那么當你定義一個返回State值的函數時它意味着什么呢? 讓我們暫時忽略State是monad的事實,只看一下底層類型。 首先,考慮一下這個函數(它實際上並沒有對狀態做任何事情):

len2State :: String -> State Int Bool
len2State s = return ((length s) == 2)

如果你看一下State的定義,我們可以看到stateData類型是Intresult類型是Bool ,所以數據構造函數包裝的函數必須是Int -> (Bool, Int) 現在,想象一下len2State無狀態版本 - 顯然,它將具有String -> Bool類型。 那么你將如何將這樣一個函數轉換成一個返回一個適合State包裝器的值?

很明顯,轉換后的函數需要采用第二個參數, 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) ,因為這就是我們的State包裝器所需要的。 覆蓋狀態值在State計算之外並不真正具有合理的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。 我們擁有完成這項任務所需的所有部分,但將其全部寫出來會很痛苦。 我們可以創建一個自動將兩個函數鏈接在一起的函數,每個函數返回類似狀態的函數嗎 當然可以! 我們只需要確保它將兩個參數作為參數:第一個函數返回的State函數,以及將previous函數的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'

所有這一切都是將第一個狀態函數應用於某些狀態數據,然后將第二個函數應用於結果和修改后的狀態數據。 簡單吧?

現在,有趣的部分:在chainStatesconvert ,我們幾乎可以將無狀態函數的任何組合轉換為啟用狀態的函數! 我們現在唯一需要的是替換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)

當然,我們不能忘記,國家實際上是一個包含類似國家的功能並使我們遠離它們的單子,因此我們所建造的任何漂亮的功能都不會幫助我們處理真實的事物。 或者他們會嗎? 令人震驚的是,事實證明,真正的國家monad以不同的名字提供了所有相同的功能:

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)

首先,您的示例過於復雜,因為它不需要將val存儲在狀態monad中; 只有種子才是持久狀態。 其次,我認為如果不使用標准狀態monad,你將獲得更好的運氣,你可以用它們的類型重新實現所有狀態monad及其操作。 我想你會通過這種方式學到更多東西。 以下是一些讓您入門的聲明:

data MyState s a = MyState (s -> (s, b))

get :: Mystate s s
put :: s -> Mystate s ()

然后你可以寫自己的連詞:

unit :: a -> Mystate s a
bind :: Mystate s a -> (a -> Mystate s b) -> Mystate s b

最后

data Seed = Seed Int
nextVal :: Mystate Seed Bool

至於你的麻煩脫糖中, do你所使用的符號是非常復雜的。 但是desugaring是一種一次性的機械程序。 盡可能接近,你的代碼應該像這樣desugar(回到你原來的類型和代碼,我不同意):

 nextVal = get >>= \ Random seed val ->
                      let seed' = updateSeed seed
                          val'  = even seed'
                      in  put (Random seed' val') >>= \ _ -> return val'

為了使嵌套結構更清晰一些,我對縮進采取了重大的自由。

你有幾個很好的回應。 我在使用狀態monad時所做的是在我的腦海中用s -> (s,a)替換State sa (畢竟,這就是它的s -> (s,a) )。

然后,您將獲得一個類似於bind的類型:

(>>=) :: (s -> (s,a)) ->
         (a -> s -> (s,b)) ->
         (s -> (s,b))

你看到bind只是一種特殊的函數組合運算符,如(.)

我在這里寫了關於州monad的博客/教程。 它可能不是特別好,但通過編寫它幫助我改善了一些事情。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM