簡體   English   中英

遞歸定義一元隨機數列表:最慣用的 Haskell,類似於純代碼

[英]Recursively defining a list of monadic random numbers: most idiomatic Haskell and analogous to pure code

我正在嘗試遞歸地制作一個隨機數列表,該列表使用前一個值來獲取下一個值(因此需要遞歸而不是 map 或 fold,而且我更喜歡使其明確,除非 map/foldr 使其相比之下變得非常簡單) .

使用純 PRNG 這是非常簡單和慣用的,在我看來(puregaussian 使用 System.Random 生成一個正常變量並且類型為puregaussian :: System.Random.RandomGen t => t -> Double -> Double -> (Double, t) )。

purecurse :: System.Random.RandomGen t => t -> Double -> [Double] -> [Double]
purecurse gen current [] = []
purecurse gen current (x:xs) = let (rand, gen2) = puregaussian gen 0 1
                                   next = current + rand
                               in current:purecurse gen2 next xs

不幸的是,在 Haskell 中,純 PRNG 似乎沒有 monadic 開發得那么好,所以我想使用像 random-fu 或 mwc-probability 這樣的庫來做同樣的事情,而且我發現工作的解決方案要么是單一的,沒有那么簡潔,或者兩者兼而有之。

這是一個使用 do 表示法的解決方案,以及為什么我對它不滿意:

import Control.Monad.Primitive
import System.Random.MWC.Probability

recurse :: PrimMonad m => Gen (PrimState m) -> [Double] -> [Double] -> m [Double]
recurse gen history@(current:_) [] = return history
recurse gen history@(current:_) (x:xs) = do
                                         rand <- (sample (normal 0 1) gen)
                                         let next = current + rand
                                         recurse gen (next:history) xs

首先,我寧願使用>>=也不願使用符號,但是我找不到綁定類型為m Doublerand變量的方法,然后將其提升以在最終情況下獲得m [Double] 似乎沒有很多文檔(我可以找到)或關於如何做這樣的事情的例子。 我想也許有必要嵌套(>>=)運算符,但這可能會使函數變得極其復雜或不可讀。 如果這是權衡,也許 do notation 只是更簡潔,但我什至無法做到這一點,並且想知道如何做。

其次,該函數要求在每次調用時傳遞整個列表,並反向返回列表(只需切換nexthistory就會破壞它)。

所以。 我希望能夠傳遞初始狀態和一個列表來遞歸返回一個一元值列表。

我想要幫助的主要問題是:是否有一種 Haskell 慣用方式來編寫這種 monadic 值的遞歸,從而生成類似於純函數結構的 monadic 列表?

我想要幫助的主要問題是:是否有一種 Haskell 慣用方式來編寫這種 monadic 值的遞歸,從而生成類似於純函數結構的 monadic 列表?

您可以分兩步完成。 讓您的遞歸函數返回“一元動作”列表,然后組合/排序這些動作。

為了便於演示,讓我們考慮一個更簡單但類似的函數。 讓我們考慮輸入而不是隨機性。 您求助的列表僅用於大小(內容被忽略),所以讓我們使用一個整數。

rc ::  Int -> [Double] -> IO [Double]
rc 0 h        = return h
rc n h@(cr:_) = do rand <- readLn :: IO Double
                   let nx = cr + rand
                   rc (n-1)(nx:h) 

這是一個類似的替代方案,可以按照您想要的方式工作

rc' ::  Int -> Double -> IO [Double]
rc' 0 cr = return []
rc' n cr = do rand <- readLn :: IO Double
              let nx = cr + rand
              xs   <- rc' (n-1) nx
              return (nx : xs)

在這里沒有做符號

rc'' ::  Int -> Double -> IO [Double]
rc'' 0 cr = return []
rc'' n cr = (readLn :: IO Double) >>= (\rand -> 
              let nx = cr + rand 
              in (rc'' (n-1) nx) >>= (\xs ->
                 return (nx : xs))) 

無論如何,您可以做的另一件事是抽象出代碼片段,而不是整體呈現。

在每一步中,您都需要當前值來生成一個新值。 所以一個 step 是一個Double -> IO Double類型的函數。 這是一個非常簡潔的類型,是 monad 世界的基礎。 您可以通過x >>= step將值綁定到x >>= step或使用step1 >=> step2組合兩個步驟。 所以讓我們一起去吧。

step :: Double -> IO Double
step cr = do rand <- readLn :: IO Double
             return (cr + rand)

這很容易理解。 您“生成”一個數字,將當前數字相加並返回結果。 並且您想要執行n這樣的步驟,因此請列出步驟。

steps :: Int -> [Double -> IO Double]
steps n = replicate n step 

現在您可以選擇如何組合它們。 例如,使用>=>折疊步驟列表將是很自然的。 你會得到這個,

runSteps :: Int -> Double -> IO Double 
runSteps n = foldr (>=>) return (steps n)

它接近您想要的但只返回最終結果,而不是在每一步累積生成的值。 下面是一個(受限的)類型(>=>)和我們想要的運算符類型(*=>)

(>=>) :: Monad m => (a -> m a) -> (b -> m  a)  -> a -> m  a
(*=>) :: Monad m => (a -> m a) -> (a -> m [a]) -> a -> m [a]

定義是,

(*=>) :: Monad m => (a -> m a) -> (a -> m [a]) -> a -> m [a]
(*=>) ac uc c = do x  <- ac c
                   xs <- uc x
                   return (x:xs) 

我實際上認為這包含了您並不特別喜歡的部分。 現在我們將其抽象為這段孤立的代碼。 即使遠離遞歸調用。 最后我們只是折疊來執行這些步驟。

execSteps :: Int -> Double -> IO [Double] 
execSteps n = foldr (*=>) (\x -> return []) (steps n) 

此函數與原始輸入的不同之處在於初始輸入是Double而不是[Double] 但這是有意義的類型。 您只會在原始函數中傳遞一個單獨的包裝雙精度值。 它會按照您的要求以“正確”的順序累積元素。

是否有一種 Haskell 慣用方式來編寫這種 monadic 值的遞歸,從而產生類似於純函數結構的 monadic 列表

通常,當需要將一元值應用於純函數時, 應用運算符,例如<$><*>可能會有所幫助。

特別是,對於列表構建,通常以遞歸方式應用運算符(:)來構建列表,例如

f [] = []
f (x:xs) = x : f xs

以前綴方式:

(:) x (f xs)

但是,(:) 是純函數,默認情況下不接受 monadic 值,但好消息是,每個數據類型都是 Monad 的實例,它也是 Applicative 的實例。 在上面提到的 Applicative 運算符的幫助下,一元值可以應用於純函數而無需任何更改。 例如,

(:) <$> (pure x) <*> (pure .f) xs

將返回一個 monadic List 而不是純列表。

回到你的問題,就個人而言,我認為你有問題的解決方案已經幾乎是一種慣用的方法(因為它簡單易讀),除了總是在history的開頭附加next random value

正如你所說, the list back in reverse更糟,當history列表已經有舊的隨機值時,不方便找出哪個是新添加的。

為了解決它,它可以稍微修改為:

recurse :: PrimMonad m => Gen (PrimState m) -> [Double] -> [Double] -> m [Double]
recurse gen history [] = return history
recurse gen history (x:xs) = do rand <- (sample (normal 0 1) gen)
                                let next = (last history) + rand
                                recurse gen (history ++ [next]) xs

如果歷史的最后一個元素是最新的隨機值,這是有道理的。

但是, (:)(++)的區別在於:( (:)是O(1),而(++)是O(N),其中N是歷史列表的長度。 last history也是 O(N) 而不是 O(1) )。

為了歸檔一個有效的解決方案,一個輔助函數可能需要引入,比如newHistory ,來構造一個新的隨機值列表:

newHistory::PrimMonad m=>Gen(PrimState m)->m Double->[Double]->m [Double]
newHistory _ _ []             = return []
newHistory gen current (x:xs) = let next = (+) <$> current <*> sample (normal 0 1) gen
                                in  (:) <$> next <*> newHistory gen next xs

如前所述,在 Applicative 運算符的幫助下,語法看起來像純函數,除了以前綴方式應用函數並使用 Applicative 運算符。

然后追加回原始history列表:

(++) <$> pure history <*> newHistory gen (pure $ last history) xs

recurse函數的應用版本如下所示:

recurse2::PrimMonad m=>Gen(PrimState m)->[Double]->[Double]->m [Double]
recurse2 gen history xs = 
    (++) <$> pure history <*> newHistory gen (pure $ last history) xs

    where newHistory::PrimMonad m=>Gen(PrimState m)->m Double->[Double]->m [Double]
          newHistory _ _ []         = return []
          newHistory gen current (x:xs) = 
            let next = (+) <$> current <*> sample (normal 0 1) gen
            in  (:) <$> next <*> newHistory gen next xs

在這種情況下,我通常會直接使用帶有類似列表界面的流媒體庫,例如流媒體 它們允許從純代碼到 monadic 的更自然的轉換,並具有額外的好處,即您不需要一次構造/使用所有結果,而是增量地,就像純列表一樣。

我不確定purecurse在做什么,但可以寫成

import           Streaming
import qualified Streaming.Prelude as S

recurse :: PrimMonad m 
        => Gen (PrimState m) 
        -> Double 
        -> [Double] 
        -> Stream (Of Double) m ()
recurse gen current [] = 
    return ()
recurse gen current (x:xs) =
    S.yield current *> -- (*>) and (>>) work like concatenation for pure lists
    lift (sample (normal 0 1) gen) >>= \rand -> 
    recurse gen (current + rand) xs

或者,更自然地使用 do-notation,如:

recurse :: PrimMonad m 
        => Gen (PrimState m) 
        -> Double 
        -> [Double] 
        -> Stream (Of Double) m ()
recurse gen current [] = 
    return ()
recurse gen current (x:xs) = 
    do S.yield current -- (*>) and (>>) work like concatenation for pure lists
       rand <- lift $ sample (normal 0 1) gen
       recurse gen (current + rand) xs

現在你可以使用像S.take這樣的S.take來生成/提取結果的一部分。 如果要獲取整個列表,可以使用S.toList_

您的問題似乎與 do-notation 和 monads 有關。 你假設有比實際更多的魔法: 了解脫糖的工作原理將幫助你在這里。

無論如何,讓我們嘗試一步一步地將非 monadic 版本轉換為 monadic。 一、類型簽名:

recurse :: PrimMonad m => Gen (PrimState m) -> Double -> [Double] -> m [Double]

我不知道你為什么在你的版本中將[Double]作為第二個參數:我們希望盡可能少地改變原始版本。 然后是第一條:

purecurse gen current [] = []
-- Goes to:
recurse gen current [] = return []

同樣,我們盡可能少地更改:在您的純代碼中,此子句中沒有發生任何影響,因此這里也不應該發生任何影響。 你猜對了接下來的兩行:

purecurse gen current (x:xs) = let (rand, gen2) = puregaussian gen 0 1
                                   next = current + rand
-- Goes to:
recurse gen current (x:xs) = do rand <- (sample (normal 0 1) gen)
                                let next = current + rand

但是最后一個把你絆倒了。 理想情況下,我們會這樣寫:

in current:purecurse gen2 next xs
-- Goes to:
current:recurse gen next xs

但它不起作用! 更重要的是,你會得到一個令人困惑的錯誤:

• Couldn't match type ‘Double’ with ‘[Double]’
  Expected type: m [Double]
    Actual type: [Double]

這可能是導致您走上錯誤道路的原因。 這個問題與列表無關:它與m (封裝 monad)有關。 當你寫current : xsxs必須是一個列表:在這個例子中,它實際上是一個m [Double] ,或者一個包含在 monad 中的列表。 有兩種方法可以解決這個問題(它們都是等價的)。 我們可以再次使用 do 符號打開列表:

rest <- recurse gen next xs
return (current : rest)

或者我們可以提升函數current :在 monad 中工作:

fmap (current:) (recurse gen next xs)

暫無
暫無

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

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