11

我正在学习单子并有几个问题。

这就是我现在的位置。请纠正我哪里错了。

  • >>=符号是一个中缀运算符。中缀运算符是接受两个参数(左侧和右侧)并返回一个值的函数。

  • >>=符号称为绑定运算符并具有签名Monad m => m t -> (t -> m u) -> m u。但是,这些类型似乎并没有在这里排列。我们得到一个类型的值,m t第二个参数是一个接受t. (我不知道如何连接这些点。)

  • 这必须意味着绑定函数能够以某种方式m从 them t中删除,以便获取 thet并将其传递给函数。

以下是我的问题:

  • m是否能够从m t只有在这种绑定运算符中才能实现的东西中删除。这个绑定运算符是否有一些特殊的特权或什么?

  • 它与状态变化有什么关系?我理解(我认为)单子的目标是“包装”副作用,以便它们与程序的其余部分隔离。但是绑定运算符在其中的作用是什么?

4

6 回答 6

12

是从“M t”中删除“M”的能力,这只有在这种绑定运算符中才有可能。

好吧,在绑定运算符中肯定是可能的,因为它的类型指定:

(>>=) :: m a -> (a -> m b) -> m b

您的 monad 的“运行”函数通常也可以执行此操作(从您的计算中返回一个纯值)。

monad 的目标是“包装”副作用,使它们与程序的其余部分隔离

唔。不,单子让我们对计算的概念进行建模。副作用计算只是这样一个概念,状态、回溯、延续、并发、事务、可选结果、随机结果、可恢复状态、非确定性......所有这些都可以描述为单子

我假设你指的是 IO monad。这是一个有点奇怪的 monad——它生成对世界状态的抽象变化序列,然后由运行时评估。Bind 只是让我们在 IO monad 中以正确的顺序对事物进行排序——然后编译器会将所有这些排序的世界修改动作转换为命令式代码,从而改变机器的状态。

不过,这对于 IO monad 来说是非常具体的,而不是一般的 monad。

于 2009-10-25T16:47:33.310 回答
10

是否能够从“M t”中删除“M”,这只有在这种绑定运算符中才有可能。这个绑定运算符是否有一些特殊的特权或什么?

Bind 绝不是一种特殊情况,但通常它会在与 monads 数据类型相同的模块中定义。因此,它可能知道(并使用)模块未导出的详细信息。通常的情况是模块导出数据类型,但不是构造函数或有关类型内部结构的其他详细信息。然后对于使用模块的代码,数据类型的内部工作是不可见的,并且该代码不能直接修改这种类型的值。

与模块内定义的函数相反,例如某些绑定运算符>>=,可以从定义它们的模块中访问他们喜欢的任何内容。因此,此类函数可能能够做“外部”函数无法做的事情。

monad是一个特例IO,因为它不是由模块定义的,而是内置在运行时系统/编译器中的。在这里,编译器了解其实现的内部细节,并公开像IO's这样的函数>>=。这些函数的实现确实享有特殊特权,因为它们存在于“程序之外”,但这是一种特殊情况,这一事实不应该从 Haskell 中观察到。

它与状态变化有什么关系?我理解(我认为)单子的目标是“包装”副作用,以便它们与程序的其余部分隔离。但是绑定运算符在其中的作用是什么?

它实际上不需要与状态更改有关,这只是可以用 moand 处理的问题之一。monad 用于让IOIO 按特定顺序执行,但通常 monad 只是将函数组合在一起的方式。

通常,一个 monad(特别是它的 bind 函数)定义了一种将某些函数组合成更大函数的方式。这种组合函数的方法在 monad 中进行了抽象。这种组合究竟是如何工作的,或者为什么要以这种方式组合函数并不重要,monad 只是指定了一种以某种方式组合某些函数的方式。(另见这个“C#程序员的Monads”答案,我基本上用例子重复了几次。)

于 2009-10-25T20:58:58.810 回答
5

以下是 type-class 的定义Monad

class  Monad m  where

    (>>=)       :: forall a b. m a -> (a -> m b) -> m b
    (>>)        :: forall a b. m a -> m b -> m b
    return      :: a -> m a
    fail        :: String -> m a

    m >> k      = m >>= \_ -> k
    fail s      = error s

类型类的每个类型实例都Monad定义了自己的>>=功能。以下是 type-in​​stance 的示例Maybe

instance  Monad Maybe  where

    (Just x) >>= k      = k x
    Nothing  >>= _      = Nothing

    (Just _) >>  k      = k
    Nothing  >>  _      = Nothing

    return              = Just
    fail _              = Nothing

正如我们所看到的,因为 的版本Maybe是专门>>=为理解类型实例而定义的,并且因为它被定义在可以Maybe合法访问data Maybe a数据构造函数的地方他们通过。NothingJust aMaybe>>=aMaybe a

为了通过一个例子,我们可以采取:

x :: Maybe Integer
x = do a <- Just 5
       b <- Just (a + 1)
       return b

脱糖后,do-notation 变为:

x :: Maybe Integer
x = Just 5        >>= \a ->
    Just (a + 1)  >>= \b ->
    Just b

评估为:

  =                  (\a ->
    Just (a + 1)  >>= \b ->
    Just b) 5

  = Just (5 + 1)  >>= \b ->
    Just b

  =                  (\b ->
    Just b) (5 + 1)

  = Just (5 + 1)

  = Just 6
于 2009-10-26T01:52:04.643 回答
4

类型确实排成一列,很有趣。就是这样。

请记住,单子也是函子。为所有仿函数定义了以下函数:

fmap :: (Functor f) => (a -> b) -> f a -> f b

现在的问题是:这些类型真的排列整齐吗?嗯,是。给定一个 froma到的函数b,那么如果我们有一个可用的环境,我们就有一个可用f的环境。afb

类比三段论:

(Functor Socrates) => (Man -> Mortal) -> Socrates Man -> Socrates Mortal

现在,如您所知,monad 是一个配备了 bind 和 return 的函子:

return :: (Monad m) => a -> m a
(=<<) :: (Monad m) => (a -> m b) -> m a -> m b

你可能不知道,它是一个带有 return 和 join 的函子:

join :: (Monad m) => m (m a) -> m a

看看我们是如何剥离的m。使用 monad m,你不能总是从m ato得到a,但你总是可以得到 from m (m a)to m a

现在看看 的第一个参数(=<<)。它是 type 的函数(a -> m b)。当你将该函数传递给 时会发生什么fmap?你得到m a -> m (m b). m a所以,用函数“映射”a -> m b给你m (m b). 请注意,这与 的参数类型完全相同join。这不是巧合。“绑定”的合理实现如下所示:

(>>=) :: m a -> (a -> m b) -> m b
x >>= f = join (fmap f x)

实际上,bind 和 join 可以相互定义:

join = (>>= id)
于 2009-10-26T03:08:06.910 回答
2

我非常建议您阅读(http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html)。它给出了一个完美的、常识性的单子存在的理由。

于 2009-10-26T01:34:10.467 回答
2

我理解(我认为)单子的目标是“包装”副作用,以便它们与程序的其余部分隔离。

它实际上比这更微妙。Monad 允许我们以非常通用的方式对排序进行建模。通常,当您与领域专家交谈时,您会发现他们会说“首先我们尝试 X,然后我们尝试 Y,如果这不起作用,那么我们尝试 Z”。当您开始用传统语言实现类似的东西时,您会发现它不适合,因此您必须编写大量额外的代码来涵盖领域专家所说的“then”一词的含义。

在 Haskell 中,您可以将其实现为一个 monad,并将“then”转换为绑定运算符。因此,例如,我曾经编写了一个程序,其中必须根据某些规则从池中分配一个项目。对于案例 1,您从池 X 中取出它。如果它是空的,那么您移到池 Y。对于案例 2,您必须直接从池 Y 中取出它。依此类推,对于十几个案例,包括您取出的一些案例池 X 或 Y 中最近最少使用的。我专门为该工作编写了一个自定义 monad,以便我可以编写:

case c of
   1: do {try poolX; try poolY}
   2: try poolY
   3: try $ lru [poolX, poolY]

它工作得很好。

当然,这包括传统的测序模型。IO monad 是所有其他编程语言都有的模型;只是在 Haskell 中它是一个明确的选择,而不是环境的一部分。ST monad 为您提供了 IO 的内存突变,但没有实际的输入和输出。另一方面,State monad 允许您将状态限制为命名类型的单个值。

对于一些真正令人费解的事情,请参阅这篇关于后向状态单子的博客文章。状态以与“执行”相反的方向传播。如果您认为这就像一个状态单子执行一条指令,然后执行下一条指令,那么“put”会将状态值及时向后发送到任何前面的“get”。实际发生的是建立了一个相互递归的函数,只有在没有悖论的情况下才会终止。我不太确定在哪里使用这样的单子,但它说明了单子是计算模型的观点。

如果您还没有准备好,那么只需将 bind 视为可重载的分号。这让你走得很远。

于 2009-10-28T21:26:56.657 回答