9

我不了解 Haskell 单子背后的确切代数和理论。然而,当我考虑一般的函数式编程时,我得到的印象是,状态将通过获取初始状态并生成它的副本来表示下一个状态来建模。这就像将一个列表附加到另一个列表时一样;两个列表都没有被修改,但第三个列表被创建并返回。

因此,将一元操作视为隐式地将初始状态对象作为参数并隐式返回最终状态对象是否有效?这些状态对象将被隐藏,这样程序员就不必担心它们并控制它们的访问方式。因此,程序员不会像十分钟前那样尝试复制表示 IO 流的对象。

换句话说,如果我们有这个代码:

main = do
    putStrLn "Enter your name:"
    name <- getLine
    putStrLn ( "Hello " ++ name )

...可以将 IO monad 和“do”语法视为代表这种代码风格吗?

putStrLn :: IOState -> String -> IOState
getLine :: IOState -> (IOState, String)
main :: IOState -> IOState

-- main returns an IOState we can call "state3"
main state0 = putStrLn state2 ("Hello " ++ name)
    where (state2, name) = getLine state1
        state1 = putStrLn state0 "Enter your name:"
4

5 回答 5

18

不,这不是一般的单子所做的。但是,关于 data type ,您的类比实际上是完全正确State s a的,它恰好是一个monad。State定义如下:

newtype State s a = State { runState :: s -> (a, s) }

...其中类型变量s是状态值,并且a是您使用的“常规”值。因此,“State monad”中的值只是一个从初始状态到返回值和最终状态的函数。应用于 的 monadic 样式State只不过是通过一系列函数自动将状态值线程化。

monad 表面上ST是相似的,但是使用魔法来允许计算具有真正的副作用,但只有这样才能从ST.

IOmonad 本质上是一个设置为“更神奇”的STmonad,具有接触外部世界的副作用,并且只有一个IO运行计算的点,即整个程序的入口点。尽管如此,在某些概念层面上,您仍然可以将其视为通过常规函数的方式将“状态”值线程化State

但是,其他 monad不一定与线程状态、排序函数或诸如此类的有任何关系。某物成为 monad 所需的操作非常笼统和抽象。例如,使用MaybeorEither作为 monad 允许您使用可能返回错误的函数,当错误发生时,monadic 样式处理从计算中转义,就像State线程化状态值一样。将列表用作 monad 会给您带来不确定性,让您可以同时将函数应用于多个输入并查看所有可能的输出,monadic 样式会自动将函数应用于每个参数并收集所有输出。

于 2010-05-12T00:41:39.750 回答
8

因此,将一元操作视为隐式地将初始状态对象作为参数并隐式返回最终状态对象是否有效?

这似乎是学习 monad 的一个常见症结,试图弄清楚一个神奇的 monad 原始汤如何同时用于表示有状态计算、可能失败的计算、非确定性计算、异常、延续、排序副作用和很快。

通过一系列有状态的计算线程化状态是满足单子法则的操作的一个示例。

您观察到Stateand IOmonad 是近亲是正确的,但是如果您尝试插入例如 list monad,您的类比就会分崩离析。

于 2010-05-12T00:09:11.053 回答
4

不是一般的 monad,而是 IO monad,是的——事实上,类型IO a通常被定义为函数类型RealWorld -> (RealWorld, a)。所以在这个视图中,putStrLnisString -> RealWorld -> (RealWorld, ())getCharis的去糖类型RealWorld -> (RealWorld, Char)——我们只是部分应用它,monadic bind 负责完全评估它并传递 RealWorld。(GHC 的 ST 库实际上包含一个非常真实的RealWorld类型,尽管它被描述为“非常神奇”并且不适合实际使用。)

但是,还有许多其他 monad 没有此属性。没有 RealWorld 被传递,例如,monads[1,2,3,4]Just "Hello".

于 2010-05-12T00:00:31.207 回答
3

绝对不。这不是一般的单子。您可以使用 monad 来隐式传递数据,但这只是一个应用程序。如果您使用这种 monad 模型,那么您将错过许多 monad 可以做的非常酷的事情。

相反,将 monad 视为表示计算的数据片段。例如,在某种意义上,隐式传递数据不是纯粹的,因为纯粹的语言坚持要求您明确所有参数和返回类型。所以如果你想隐式地传递数据,你可以这样做:定义一个新的数据类型,它表示做一些不纯的事情,然后编写一段代码来处理它。

一个极端的例子(只是一个理论上的例子,你不太可能想要这样做)是这样的:C 允许不纯的计算,所以你可以定义一个代表一段 C 代码的类型。然后,您可以编写一个解释器,它采用这些 C 结构之一并对其进行解释。所有monad 都是这样,尽管通常比 C 解释器简单得多。但这种观点更强大,因为它还解释了与传递隐藏状态无关的 monad。

您可能还应该尝试抵制将 IO 视为绕过隐藏世界状态的诱惑。IO 的内部实现与你应该如何看待 IO 无关。IO monad 是关于构建你想要执行的 I/O 的表示。您将这些表示之一返回到 Haskell 运行时系统,然后由该系统根据需要来实现您的表示。

任何时候你想做某事,你看不到如何直接以纯粹的方式实现它,但你可以看到如何构建一个纯粹的数据结构来描述你想要的,你可能有一个monad的应用程序,特别是如果您的结构自然是某种树。

于 2010-05-12T18:00:32.780 回答
1

我更喜欢将 monad 视为代表延迟操作(runXXX,main)的对象,其结果可以根据该结果进行组合。

return "somthing"
actionA >>= \x -> makeActionB x

而那些动作不一定是全状态的。即你可以像这样考虑函数构造的monad:

instance Monad ((->) a) where
    m >>= fm = \x -> fm (m x) x
    return = const

sqr = \x -> x*x
cube = \x -> x*x*x

weird = do
    a <- sqr
    b <- cube
    return (a+b)
于 2010-05-12T05:41:45.297 回答