5

我试图了解读者和/或状态单子的实际需求。我见过的所有示例(包括许多在 stackoverflow 上的示例,因为我一直在寻找可以使用的合适示例以及在各种书籍和博客文章中)的形式(伪代码)

 f = do
        foo <- ask
        do something with foo

 g = do
        foo <- ask
        do something else using foo

 h = runReader
       (
           f
           g
       )

换句话说,调用两个函数并(大概)从一个调用到下一个调用保持某种状态。但是,我不觉得这个例子特别有说服力,因为(我认为)我可以让 f 返回一些状态,然后将该状态传递给 g。

我希望看到一个示例,使用单个整数(例如)作为要保留的状态,而不是从中心位置对 f 和 g 的两次连续调用,而是对 f 的调用,然后在内部调用 g然后在主例程中更改了可用的状态(如果是状态单子)。

我见过的大多数(实际上是所有)示例都花费大量时间关注 monad 的定义,然后展示如何设置单个函数调用。对我来说,这将是进行嵌套调用并让状态随身携带以证明它为什么有用的能力。

4

3 回答 3

15

这是一个有状态子例程调用另一个有状态子例程的重要示例。

import Control.Monad.Trans.State

f :: State Int ()
f = do
    r <- g
    modify (+ r)

g :: State Int Int
g = do
    modify (+ 1)
    get

main = print (execState f 4)

在此示例中,初始状态从 开始,4有状态计算从 开始ff内部调用g,它将状态增加到5然后返回当前状态(仍然5)。这会将控制恢复到f,将值绑定5r,然后将当前状态增加r,最终状态为10

>>> main
10
于 2014-07-04T15:41:19.253 回答
9

几乎所有你可以用 monad 做的事情,你可以不用它们做。(嗯,有些是特殊的,例如ST, STM,IO等,但那是另一回事。)但是:

  • 它们允许您封装许多常见模式,例如在这种情况下的状态计算,并隐藏原本需要的细节或样板代码;和
  • 有许多函数可以在任何(或许多)monad 上工作,您可以专门针对您正在使用的特定 monad。

举个例子:通常需要某种生成器来提供唯一名称,例如在生成代码时等。这可以使用 state monad 轻松完成:每次newName调用时,它都会输出一个新名称并递增内部状态:

import Control.Monad.State
import Data.Tree
import qualified Data.Traversable as T

type NameGen = State Int

newName :: NameGen String
newName = state $ \i -> ("x" ++ show i, i + 1)

现在假设我们有一棵有一些缺失值的树。我们想为他们提供这样生成的名称。幸运的是,有一个通用函数mapM允许使用任何 monad 遍历任何可遍历的结构(如果没有 monad 抽象,我们就没有这个函数)。现在修复树很容易。对于每个值,我们检查它是否被填充(然后我们只是return用来将它提升到 monad 中),如果没有,则提供一个新名称:

fillTree :: Tree (Maybe String) -> NameGen (Tree String)
fillTree = T.mapM (maybe newName return)

想象一下在没有 monad 的情况下实现这个函数,有明确的状态 - 手动遍历树并携带状态。最初的想法将完全丢失在样板代码中。此外,该功能将非常特定于TreeNameGen

但是有了单子,我们可以走得更远。我们可以参数化名称生成器并构造更通用的函数:

fillTreeM :: (Monad m) => m String -> Tree (Maybe String) -> m (Tree String)
fillTreeM gen = T.mapM (maybe gen return)

注意第一个参数m String。它不是一个常数值,它是在需要时String生成新的 inside 的秘诀。Stringm

那么原来的可以改写为

fillTree' :: Tree (Maybe String) -> NameGen (Tree String)
fillTree' = fillTreeM newName

但是现在我们可以将同一个函数用于许多非常不同的目的。例如,使用Randmonad并提供随机生成的名称。

或者,在某些时候,我们可能会认为没有填写节点的树是无效的。然后我们只是说,无论何时我们被要求提供一个新名称,我们都会中止整个计算。这可以实现为

checkTree :: Tree (Maybe String) -> Maybe (Tree String)
checkTree = fillTreeM Nothing

这里Nothing是 type Maybe String,而不是尝试生成新名称,而是中止Maybemonad 中的计算。

如果没有单子的概念,这种抽象级别几乎是不可能的。

于 2014-07-04T20:13:39.793 回答
3

我试图了解读者和/或状态单子的实际需求。

一般来说,有很多方法可以理解单子,特别是这些单子。在这个答案中,我专注于对这些单子的一种理解,我认为这可能对 OP 有帮助。

reader 和 state monad 为非常简单的使用模式提供库支持:

  • reader monad 支持将参数传递给函数。
  • state monad 支持从函数中获取结果并将它们传递给其他函数。

正如 OP 正确地指出的那样,对这些东西的库支持并不大,这在 Haskell 中已经很容易了。如此多的 Haskell 程序可以使用 reader 或 state monad,但这样做没有意义,所以他们不这样做。

那么为什么有人会使用阅读器或状态单子呢?我知道三个重要的原因:

  1. 现实的程序包含许多相互调用并来回传递信息的函数。有时,许多函数接受的参数只是传递给其他函数。reader monad 是这种“接受参数并传递它们”模式的库。state monad 是一个类似“接受参数,传递它们,并将结果作为我的结果传回”模式的库。

    在这种情况下,使用 reader 或 state monad 的好处是参数会自动传递,我们可以专注于这些函数更有趣的工作。代价是我们必须使用 monadic 样式(do notation 等)。

  2. 现实的程序可以一次使用多个 monad。他们需要传递的参数,返回的参数,错误处理,非确定性,......

    在这种情况下,使用 reader 或 state monad转换器的一个好处是我们可以将所有这些 monad 打包到单个 monad 转换器堆栈中。我们仍然需要 monadic 风格,但现在我们只需支付一次成本(到处使用 do 表示法)并经常获得收益(transformer stack 中有多个 monad)。

  3. 一些库函数适用于任意 monad。例如,sequence :: [m a] -> m [a]接受一个单子动作列表,按顺序运行所有动作,并返回收集的结果。

    使用 reader 或 state(或其他)monad 的一个好处是我们可以使用这些适用于任何 monad 的非常通用的库函数。

请注意,第 1 点和第 2 点只出现在实际的、有点大的程序中。所以很难举一个小例子来说明使用 monad 的好处。第 3 点出现在小型库函数中,但更难理解,因为这些库函数通常非常抽象。

于 2014-07-04T19:18:48.773 回答