【问题标题】:Recursively defining a list of monadic random numbers: most idiomatic Haskell and analogous to pure code递归定义一元随机数列表:最惯用的 Haskell,类似于纯代码
【发布时间】:2019-04-14 18:02:34
【问题描述】:

我正在尝试递归地制作一个随机数列表,该列表使用前一个值来获取下一个值(因此需要递归而不是 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 中的开发似乎不如 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

首先,我宁愿使用 &gt;&gt;= 而不是 do 表示法,但是我找不到绑定 rand 类型为 m Double 的变量然后将其提升到最后得到 m [Double] 的方法案子。似乎没有很多文档(我可以找到)或有关如何执行此类操作的示例。 我想也许有必要嵌套 (&gt;&gt;=) 运算符,但这可能会使函数变得非常复杂或不可读。如果这是权衡,也许 do 表示法更干净,但我什至没有设法使这项工作,我想知道如何。

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

所以。我希望能够传递初始状态和一个递归列表,该列表返回一个单子值列表。

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

【问题讨论】:

  • 输入列表的目的是什么?在这些示例中,您从不使用它的元素 (x)。如果你想使用标准的脱糖:sample (normal 0 1) gen &gt;&gt;= \rand -&gt; let next = current + rand in recurse gen (next : history) xs,或者省略let 绑定,sample (normal 0 1) gen &gt;&gt;= \rand -&gt; recurse gen (current + rand : history) xs,你可以用&gt;&gt;= 编写你的do-notation 版本。
  • 你说得对,x 值实际上并没有被使用,它们可能在这个函数的不同版本中,我经常更喜欢倒计时到 0 作为边缘情况。脱糖是有道理的,而且它有效,但我想有一些奇怪的结构与放置不那么直观的回报。我仍然需要找到一种方法来取消 history@ 构造,但这应该比第一部分更容易。谢谢。
  • 相关:“在 monad 中构建列表”joachim-breitner.de/blog/620-Constructing_a_list_in_a_Monad
  • 单子没有什么“不纯”的;它们只是允许您在对purecurse 的调用之间抽象出对生成器的显式处理。 monad 不纯的概念源于这样一个事实,即人们通常通过使用 IO 动作来了解 monad。例如,getLine :: IO String 是一个 IO action,但 putStrLn :: String -&gt; IO () 是一个返回 IO 操作的 函数。 &gt;&gt;= 操作符是一个纯函数,它创建一个新的 IO 操作,例如,getLine &gt;&gt;= putStrLn :: IO ()
  • @chepner 是的,我意识到,关键不在于函数是否纯,而是如何使用翻译/将典型的递归函数改编为使用 monad 的函数。谢谢。

标签: list haskell recursion random monads


【解决方案1】:

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

您可以分两步完成。让您的递归函数返回“一元动作”列表,然后组合/排序这些动作。

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

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 -&gt; IO Double 类型的函数。这是一种非常简洁的类型,在单子世界中是基本的。您可以通过x &gt;&gt;= step 将值绑定到一个步骤或使用step1 &gt;=&gt; 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 

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

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

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

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

【讨论】:

  • 我试图制作一系列我认为你称之为“单子动作”的东西,以将结果绑定到 m Double 变量,但我无法像我想的那样把它变成 m [Double]可以。我假设序列将在单子列表上“正常工作”,因此我可以将其绑定到m [Double],但我仍然缺少上一步。我不确定这是否是最接近纯函数结构的解决方案,但无论如何,一个完整的例子将不胜感激。谢谢。
  • 我编辑了我的回复以提供一个示例。我已经明确提到了“序列”,但实际上你的函数不能以这种方式排序。在任何情况下,都是相同的(您可能喜欢也可能不喜欢)。您可以将代码的各个部分抽象出来,从而产生一些您需要排序/组合的操作。这个想法是,这应该隔离你发现更混乱的一个或多个位,其余的应该很容易与非单子策略相关。
  • 感谢您的详细说明。我不清楚这个实现是否解释了每个新状态对前一个状态的依赖,而不仅仅是累加器和新的“随机”值,但不可否认,我不理解单子运算符&gt;=&gt;*=&gt;。我不是一个经验丰富的 Haskeller,很少使用 monads 超越 &gt;&gt;=&gt;&gt;。另外我认为单片递归非常透明,所以我希望有一种类似的方法可以做到这一点。然而,这很可能是更惯用的,但我真的不喜欢它。无论如何感谢您的帮助。
  • 好的,所以编辑了答案以包含一个整体替代您的答案。我想你可能更喜欢那个。我确实相信通过模块化代码应该非常有用。特别是因为单体版本隐含地完成了 (*=>) 的工作。你可能会更好地理解这一点。此外,模块化代码使您有机会查看带有 (>=>) 的版本,它只返回最后一个结果,这更简单,也是了解这一切如何工作的中间步骤。还要注意 (>=>) 非常接近 (>>=),它只是由 (f >=> g) x = f x >>= g 定义的
  • rc 函数是我想要的一半,与我上面的评论相同,另一部分是使用 &gt;&gt;= 而不是 do,但我似乎没有对多行语句进行脱糖正确,如果可能的话。那将是我正在寻找的确切答案。
【解决方案2】:

有没有一种 Haskell 惯用的方式来编写这样的递归 monadic values 产生一个类似于 纯函数的结构

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

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

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

前缀方式:

(:) x (f xs)

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

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

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

回到您的问题,就我个人而言,我认为您的解决方案已经几乎是一种惯用的方法(因为它简单易读),除了总是在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

【讨论】:

  • 谢谢。我认为 applicative 可以在这里替代 monads,我将不得不仔细看看使用它会产生什么,但我希望即使对于非 Haskellers 也尽可能保持可读性,并且 applicative 总是看起来有点陌生对很多人(公平地说,&gt;&gt;=)。再次感谢。
【解决方案3】:

您的问题似乎在于 do-notation 和 monads。您假设发生的魔法比实际情况要多得多:learning how the desugaring works 会在这里为您提供帮助。

无论如何,让我们尝试一步一步地将非单子版本转换为单子版本。一、类型签名:

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(封装单子)有关。当你写current : xs 时,xs 必须是一个列表:在这个例子中,它实际上是一个m [Double],或者是一个包裹在 monad 中的列表。有两种方法可以解决这个问题(它们都是等价的)。我们可以展开列表,再次使用 do 表示法:

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

或者我们可以提升函数current :在monad中工作:

fmap (current:) (recurse gen next xs)

【讨论】:

  • 谢谢,但我确实了解脱糖的工作原理,并且 do-notation 函数正在执行预期的操作。我确实在绑定运算符和递归的交互方面遇到了麻烦,这就是我需要帮助的地方,但感谢您的时间。
【解决方案4】:

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

我不确定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.toList_

【讨论】:

  • pureceurse 是一个典型的递归定义列表,其中每个项目都在一个步骤中定义并使用: 连接到前面的项目,然后在最后附加:[] 以实际使其成为一个列表。如果可能的话,我想要一个单子版本的基本单子运算符,而不是更高级别的库,甚至不做符号。无论如何感谢您的帮助,我会检查一下。
猜你喜欢
  • 2013-03-16
  • 1970-01-01
  • 2018-02-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-22
  • 2014-02-13
  • 1970-01-01
相关资源
最近更新 更多