还有一次......我可以举一个做我想要的状态monad的例子吗?

时间:2014-07-04 15:05:49

标签: haskell state-monad reader-monad

我正在努力了解读者和/或州monad的实际需求。我见过的所有例子(包括很多关于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然后调用f在内部调用g,然后在主例程中更改状态(如果状态monad)。

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

3 个答案:

答案 0 :(得分:13)

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

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开始,有状态计算从f开始。 f在内部调用g,将状态递增到5,然后返回当前状态(仍为5)。这会将控制恢复为f,将值5绑定到r,然后将当前状态增加r,最终状态为10:< / p>

>>> main
10

答案 1 :(得分:7)

几乎所有你可以用monads做的事情,没有它们你可以做。 (好吧,有些是特别的,如STSTMIO等,但这是一个不同的故事。)但是:

  • 它们允许您封装许多常见模式,例如在这种情况下进行有状态计算,并隐藏其他需要的细节或样板代码;和
  • 有很多功能适用于任何(或许多)monad,你可以专注于你正在使用的特定monad。

举一个例子:通常需要某种类型的生成器提供唯一的名称,比如生成代码等。这可以使用状态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)

现在让我们说a tree有一些缺失值。我们希望为他们提供这样生成的名称。幸运的是,有一个泛型函数mapM允许遍历任何monad的任何可遍历结构(没有monad抽象,我们不会有这个函数)。现在修复树很容易。对于每个值,我们检查它是否已填充(然后我们只使用return将其提升到monad中),如果没有,则提供一个新名称:

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

想象实现这个没有monad的函数,具有显式状态 - 手动通过树并携带状态。最初的想法将在样板代码中完全丢失。此外,该功能对TreeNameGen非常具体。

但是对于monad,我们可以走得更远。我们可以参数化名称生成器并构建更通用的函数:

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

请注意第一个参数m String。它不是常量String值,它是在String内生成新m的方法,无论何时需要。

然后原始的可以像

一样重写
fillTree' :: Tree (Maybe String) -> NameGen (Tree String)
fillTree' = fillTreeM newName

但是现在我们可以将相同的功能用于许多不同的目的。例如,使用Rand monad并提供随机生成的名称。

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

一样实现
checkTree :: Tree (Maybe String) -> Maybe (Tree String)
checkTree = fillTreeM Nothing

其中Nothing的类型为Maybe String,而不是尝试生成新名称,而是中止Maybe monad中的计算。

如果没有monad的概念,这种抽象水平几乎是不可能的。

答案 2 :(得分:3)

  

我试图了解读者和/或州monad的实际需求。

有很多方法可以理解monad,特别是这些monad。在这个答案中,我专注于对这些monad的理解,我认为这可能对OP有所帮助。

读者和州monad为非常简单的使用模式提供了库支持:

  • 读者monad支持将参数传递给函数。
  • 状态monad支持从函数中获取结果并将其传递给其他函数。

正如OP正确地指出的那样,不需要对这些东西提供库支持,这在Haskell中已经非常容易了。如此多的Haskell程序可以使用读者或州monad,但这样做没有意义,所以他们不会。

那么为什么有人会使用读者或州monad呢?我知道三个重要原因:

  1. 真实的程序包含许多相互调用并来回传递信息的函数。有时,许多函数接受仅传递给其他函数的参数。读者monad是一个用于此接受参数的库,并将它们传递给&#34;图案。状态monad是一个类似&#34;接受参数的库,传递它们,并将结果作为我的结果传回去#34;图案。

    在这种情况下,使用reader或state monad的好处是参数会自动传递,我们可以专注于这些函数中更有趣的工作。成本是我们必须使用monadic风格(做符号等)。

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

    在这种情况下,使用阅读器或状态monad 变换器的好处是我们可以将所有这些monad打包成单个monad变换器堆栈。我们仍然需要monadic风格,但现在我们支付一次成本(在任何地方使用符号)并经常获得好处(变压器堆栈中有多个monad)。

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

    使用阅读器或州(或其他)monad的好处是我们可以使用适用于任何monad的这些非常通用的库函数。

  4. 请注意,第1点和第2点仅显示在实际的,有点大的程序中。因此很难给出使用monad的这个好处的一个小例子。第3点显示在小型库函数中,但更难理解,因为这些库函数通常非常抽象。