6

我是haskell、函数式语言和monads的新手。
我已经搞砸了大约一个月;我已经阅读 了你的 haskell 并且正在尝试制作我的 haskell 网站。

但是有一些事情让我感到困扰:monads 抽象。如果我理解正确,单子是可以排序的“数据容器”。例如,我可以用“>>=”“解包”它,并且将为我“在幕后”完成更多工作,因此如果我没有 monad 定义,我必须猜测它将如何解包。

例如:

我们有一个列表单子,解压它会对其元素进行排序

[1,2,3] >>= return . (+1) -- gives back [2,3,4]

或者更复杂的 monad,例如这些示例中的 writer: Log Writer Monad

或者我可能有一个 webWriter monad,对于它的每个“解包”它的值,它都会向某个远程服务器发送一个请求(我不确定这个,但我试图给出一个极端的情况)

我的问题是:我能否仅通过查看 monad 用户界面(我猜是类型定义)来判断 apply 函数('>>='、'applyLog')在幕后做什么?

希望我能很好地解释自己。

谢谢,奥伦。

4

7 回答 7

8

虽然您不能仅通过查看界面就知道(>>=)特定 monad 的作用,但每个 monad 都必须遵守规则才能构成“适当的”monad。这限制了return和的可能实现(>>=)单子定律如下:

  • 左身份: return a >>= f等于f a
  • 正确的身份: m >>= return等于m
  • 关联性: (m >>= f) >>= g等于m >>= (\x -> f x >>= g)

例如,如果returnList monad 被定义为\x -> [x,x]而不是\x -> [x],那将违反左恒等律。return 5 >>= \x -> [x+1]将不同于(\x -> [x+1]) 5.

此外,并非所有 monad 都可以直观地理解为某种“容器”。容器类比适用于 List 和 Maybe,但是 Reader 呢?Reader 值并不真正“包含”任何东西。相反,它是对依赖于外部不变环境的计算的描述。

Monad 是任何实现 monad 接口并遵守 monad 法则的东西。

编辑:作为如何直观地了解 monad 实例对给定类型所做的示例,请考虑Streams 包Data.Stream.Infinite.Stream。流就像列表,只是它们总是无限的。

Stream 有一个 Monad 实例。在这种情况下会做什么return(>>=)

return有类型a -> Stream a。这种类型唯一可能的函数是返回作为参数传递的值的无限重复的函数。

(>>=)更棘手。它有类型Stream a -> (a -> Stream b) -> Stream b。这种类型的一个可能的函数是接受第一个参数的头部并将其应用于第二个参数,返回结果流的函数。s >>= f = f $ head s.

另一种可能的实现是(>>=)将 type 的函数应用于a -> Stream b原始流的每个元素,获得 type 的中间结果Stream (Stream b),然后以某种方式将流的流折叠为单个Stream b值。怎么做?您可以简单地取无限正方形的对角线!

哪个版本(>>=)与单子定律兼容?第一个当然不会,因为它破坏了正确的身份。的结果1,2,3,4... >>= return将是1,1,1,1...。第二个实现尊重正确的身份(你能明白为什么吗?),这让我们更加确信它可能是实现(>>=)流的正确方法。当然,您需要所有法律的实际证明才能确定!

于 2013-09-10T06:13:10.997 回答
4

我描述了四种信息来源,您可以使用它们来了解>>=特定 monad 的行为。

的类型>>=

的类型>>=始终相同。它在Monad类型类中指定。请参阅文档。类型是:

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

您感兴趣的特定 monad 的占位符在哪里m。例如,对于列表 monad,其类型>>=为:

(>>=) :: forall a b. [a] -> (a -> [b]) -> [b]

请注意,我刚刚替换m ...[...].

单子定律

每个 monad的实现>>=都不同,但所有 monad 都应该遵守 monad 法则。这些定律在Monad类型类的文档中指定。再次查看文档。法律是:

  1. return a >>= k == k a
  2. m >>= return == m
  3. m >>= (\x -> k x >>= h) == (m >>= k) >>= h

因此,无论某些特定 monad 的实现可能是什么,您都可以使用这些定律来推理您的代码。例如,如果您的代码包含类似法律左侧的代码,您可以用相应的法律右侧的代码替换该代码,并且行为不应改变。

这是一个如何使用单子定律的示例。假设我写了这段代码:

foo = do
  x <- bar
  return x

我们甚至不知道这里使用了什么 monad,但我们知道有一些 monad,因为我们看到了 do 表示法。要应用 monad 法则,我们必须对调用的 do 表示法去糖>>=

foo = bar >>= (\x -> return x)

请注意,\x -> return x这与 just 相同return(通过 η-reduction.

foo = bar >>= return

根据第二个单子定律,这段代码与调用 bar 的含义完全相同。

foo = bar

所以看起来好像>>=原始foo函数中的 根本不能做任何有趣的事情,因为单子定律允许我们把它排除在外。我们甚至不知道>>=这里的操作员提供了什么特定的 monad 就知道了。

特定单子的文档

如果您需要了解有关特定 monad 的行为的更多信息,>>=特定 monad 的文档应该会告诉您。您可以使用hoogle搜索文档。例如,文档StateT告诉您:

return函数保持状态不变,同时>>=使用第一次计算的最终状态作为第二次计算的初始状态。

特定monad的实现

如果您想了解有关特定 monad 实现的更多细节,您可能必须查看实际实现。搜索instance Monad ...声明。例如,查看. StateTlist monad 的实现在这个文件的某个地方,搜索instance Monad []或查看这个,除了:

instance  Monad []  where
    m >>= k             = foldr ((++) . k) [] m
    m >> k              = foldr ((++) . (\ _ -> k)) [] m
    return x            = [x]
    fail _              = []

也许不是最明显的定义,但如果你调用>>=list monad,就会发生这种情况。

概括

所有 monad 共享>>=return和 monad 法则的类型签名。除了这些限制之外,每个 monad 都提供了不同的>>=and实现return,如果你想知道所有细节,你必须研究instance Monad ...声明的源代码。如果您只想学习如何使用特定的 monad,请尝试查找一些相关文档。

于 2013-09-10T06:35:58.343 回答
3

monad 不是“数据容器”。monad 是一种高阶计算结构。如果您考虑 <=<,则可以更好地理解 >>= 的含义。

f . g-- 简单的函数组合

mf <=< mg-- 计算的组成。

在我看来,这更能说明问题。但是,<=< 可以通过>>= 来定义,所以通常只有>>= 需要定义。您还可以通过 <=< 定义 >>=:m >>= f = (f <=< const m) ()

“m a”不是数据容器。它只是说它实现了类似“a”的行为——所以现在我们只能将正确类型的部分组合在一起。(>>=) :: m a -> (a -> m b) -> m b告诉我们,因为“m a”暴露了类似“a”的行为,我们可以加入一个使用类似“a”的行为的函数变成类似“b”的行为。

它如何为任何类型实现类似“a”的行为?嗯,这就是我们说它是函子的原因:它将任何函数 a->b 映射到 m a->mb - 对于每个函数 a->b 它找到(或构建)一个函数,如果 f 和 g compose,然后 mf 和 mg 也组成。这是关于保留类型 a 和 b 的代数性质的有力陈述:如果 f 加 1,而 g 减 1,则得到相同的数;然后用 mg 组成的 mf 也会让你回到你开始的地方——它可能没有存储在任何地方的数字,但行为将类似于“相同的数字”。

此外,作为 monad,意味着它只关注高阶结构,而不是实际类型:因为“m a”可以具有任何类型 a,这意味着实现不能依赖于类型的细节。monad 只能使用计算的代数结构——广义上的“代数”。例如,列表 [a] 可以包含任何元素,但 monad 只能使用列表的代数结构进行操作 - 枚举元素、拆分列表、折叠等,但不能将所有元素相加- 加起来是“a”-like。其他 monad 也将具有特定于 monad 的功能;没有它们,它们可能毫无用处 - 例如,, ,ask等。atomicallyretry

于 2013-09-10T07:26:27.533 回答
2

仅查看 monad API 的类型签名与查看函数的类型签名相同,例如 : a -> b -> c,它只会告诉您,给定某些东西,该函数可以给您其他东西。函数如何做到这一点是函数的实现细节。

同样,bind,return和其他 monad 特定函数(如State monad 中的 ,​​ )仅让您了解put使用这些函数可以完成从一件事到另一件事的所有映射。如果您需要了解 monad 实际工作的基本逻辑您可以查看文档(如果提供)或源代码。get

于 2013-09-10T04:19:51.160 回答
2

我能告诉apply函数在幕后做什么吗?

对于一个你没有问的问题,有很多完全准确的答案。你能从 monad 的使用上下文中看出它在做什么吗?不,一般不会。

您必须希望 monad 的名称及其文档(恐怕通常非常稀疏)有所帮助,并且周围的代码(通常非常简洁)为您提供上下文。

当然,如果是您熟悉的代码或代码库,那就很简单了。然而,大多数 Haskell 代码似乎并没有被优化为“可略读”。

于 2013-09-10T06:51:54.983 回答
1

这是一个有趣的问题。我想说你对单子可以做什么有一个很好的理解。我想说,如果不阅读文档,就不可能知道任何函数的具体行为。每个 monad 的绑定和返回都是专门针对类型的结构和目的实现的。

于 2013-09-10T04:20:18.083 回答
1

仅通过查看它们的类型签名,您无法准确判断给定 monad 会做什么return或将要做什么。>>=那是因为他们的类型签名将永远

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

非常通用。这在类型级别上很棒,因为它是函数在那里做什么的精确规范。

在更精细的分辨率下,您必须查看 monad 实例声明。

于 2013-09-10T04:21:38.713 回答