[英]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 Double
的rand
變量的方法,然后將其提升以在最終情況下獲得m [Double]
。 似乎沒有很多文檔(我可以找到)或關於如何做這樣的事情的例子。 我想也許有必要嵌套(>>=)
運算符,但這可能會使函數變得極其復雜或不可讀。 如果這是權衡,也許 do notation 只是更簡潔,但我什至無法做到這一點,並且想知道如何做。
其次,該函數要求在每次調用時傳遞整個列表,並反向返回列表(只需切換next
, history
就會破壞它)。
所以。 我希望能夠傳遞初始狀態和一個列表來遞歸返回一個一元值列表。
我想要幫助的主要問題是:是否有一種 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 : xs
, xs
必須是一個列表:在這個例子中,它實際上是一個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.