递归定义一元随机数列表:最惯用的 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

不幸的是,纯 PRNG 在 Haskell 中似乎不如单子 PRNG 开发得那么好,所以我想使用像 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 表示法更简洁,但我什至没有设法做到这一点,我想知道如何做到。

其次,该函数要求在每次调用时传递整个列表,并反向返回列表(只需切换 nexthistory 即可打破它)。

所以。我希望能够传递初始状态和一个列表以递归 returns 一个单值列表。

我想得到帮助的主要问题是:是否有一种 Haskell 惯用的方式来编写这种单子值的递归,从而产生类似于纯函数结构的单子列表?

The main question I would like help with is: is there a Haskell idiomatic way of writing such a recursion of monadic values resulting in a monadic list that is similar to the structure of a pure function?

您可以分两步完成。让你的递归函数 return 有一个 "monadic actions" 的列表,然后组合/排序这些动作。

让我们考虑一个更简单但与您的函数类似的函数,以便于演示。让我们考虑输入而不是随机性。您追索的列表仅用于大小(内容被忽略)所以我们只使用一个整数。

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))) 

无论如何,您可以做的另一件事是抽象出代码片段,而不是拥有单一的表示。

在每个步骤中,您都需要当前值来生成新值。因此,一个步骤是 Double -> IO Double 类型的函数。这是一个非常简洁的类型,是 monad 世界的基础。您可以通过 x >>= step 将值绑定到一个步骤或使用 step1 >=> step2 组合两个步骤。所以我们开始吧。

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

非常容易理解。您 'generate' 一个数字,将当前数字与 return 结果相加。而你想要做 n 这样的步骤,所以列一个步骤列表。

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

现在您可以选择如何组合它们。例如,用 >=> 折叠步骤列表是很自然的。你会得到这个,

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

它接近你想要的但只是 return 最终结果,而不是在每一步累积生成的值。下面是一个(受限的)类型(>=>)和我们想要的运算符类型(*=>)

(>=>) :: 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]。但这是有意义的类型。您只需在原始函数中传递一个包装好的双精度值。并且它会按照您的要求累积 'right' 顺序中的元素。

在这种情况下,我通常会直接使用具有适当的类似列表界面的流式库,例如 streaming。它们允许从纯代码到 monadic 的更自然的转换,并且具有额外的好处,即您不需要一次 construct/consume 所有结果,而是增量地,就像纯列表一样。

我不确定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 to generate/extract only parts of the result. If you want to get the whole list, you can use S.toList_ 的函数了。

is there a Haskell idiomatic way of writing such a recursion of monadic values resulting in a monadic list that is similar to the structure of a pure function

通常,当需要将一元值应用于纯函数时,Applicative 运算符,例如 <$><*> 可能会有所帮助。

特别是对于列表构造,经常以递归的方式应用运算符(:)来构建列表,如

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

前缀方式:

(:) x (f xs)

然而,(:)是纯函数,默认不接受monadic值,但好消息是,每个数据类型都是Monad的实例,它也是Applicative的实例。在上面提到的 Applicative 运算符的帮助下,monadic value 可以应用于纯函数而无需任何更改。例如,

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

将return一个单子列表而不是纯列表。

Return 对于您的问题,就我个人而言,我认为您的解决方案几乎是一种惯用的方法(因为它简单易读),除了总是在头部附加 next random value history

正如您所说,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

如前所述,在应用运算符的帮助下,语法看起来像纯函数,除了以前缀方式应用函数并使用应用运算符。

然后追加回原来的 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

您的问题似乎与 do-notation 和 monad 有关。您假设发生的事情比实际情况要多得多:learning how the desugaring works 会帮助您。

无论如何,让我们一步步尝试将非 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)