【问题标题】:Monads And Abstraction单子和抽象
【发布时间】:2013-09-13 15:37:42
【问题描述】:

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

但有一点让我很困扰:monad 抽象。 如果我理解正确,单子是可以排序的“数据容器”。 例如,我可以用“>>=”“解包”它,更多的工作将在“幕后”完成 对我来说,所以如果我没有 monad 定义,我必须猜测它是如何解包的。

例如:

我们有列表 monad,解压后会对其元素进行排序

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

或者像这些示例中的作者这样更复杂的 monad: Log Writer Monad

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

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

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

谢谢,奥伦。

【问题讨论】:

  • 不,您无法仅通过查看它的类型来判断 any 函数在幕后做了什么。这适用于像head 这样的“普通函数”和像(>>=) 这样的monad 函数。 (有时您实际上可以在给定类型签名和there is a tool that does this 的情况下提供单个有效实现,但目前可能超出您的理论水平。)

标签: haskell monads


【解决方案1】:

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

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

例如,如果将 List monad 的 return 定义为 \x -> [x,x] 而不是 \x -> [x],则将违反左身份定律。 return 5 >>= \x -> [x+1](\x -> [x+1]) 5 不同。

另外,not all monads can be intuitively understood as 'containers' of some kind。容器类比适用于 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

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

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

【讨论】:

  • 感谢您的快速回答,如果我的 monad 不会打破这些角色,但仍然会做一些“有趣”的事情,比如向远程主机发出请求,这意味着:如果我的 monad 返回为 \x -> [x] 不是 [x,x] 但在返回之前它会执行一些 IO 操作,例如写入磁盘到网络...额外
  • 它不一定是 IO,它可以是真正的资源消耗计算。
  • @Oren 大多数单子都是“纯”的。 List monad 只允许不确定性,Maybe monad 只允许失败,等等。您不能在 list monad 中执行 I/O! (好吧,如果您在 IO monad 上使用 ListT monad 转换器,则可以,但这并不是一回事)。至于可能永远不会停止或消耗过多资源的非常长的计算,Haskell 不够“聪明”,无法可靠地检测它们并在类型系统中“标记”它们。像 Adga 这样的其他语言确实有所谓的“终止检查器”。
【解决方案2】:

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,则得到相同的数;然后 m f 与 m g 组成也会让你回到你开始的地方——它可能没有存储在任何地方的数字,但行为将是“相同的数字”。

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

【讨论】:

    【解决方案3】:

    我能告诉应用函数...在幕后做什么

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

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

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

    【讨论】:

      【解决方案4】:

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

      >>=的类型

      >>= 的类型始终相同。它在Monad 类型类中指定。见documentation。类型是:

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

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

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

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

      单子定律

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

      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 法则,我们必须对 &gt;&gt;= 的调用去除 do 符号:

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

      请注意,\x -&gt; return xreturn 相同(通过 η-reduction。

      foo = bar >>= return
      

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

      foo = bar
      

      所以看起来好像原始foo 函数中的&gt;&gt;= 根本不能做任何有趣的事情,因为单子法则允许我们把它排除在外。我们甚至在不知道这里提供 &gt;&gt;= 运算符的特定 monad 的情况下就知道了这一点。

      特定 monad 的文档

      如果您需要了解更多关于特定 monad 的 &gt;&gt;= 的行为,特定 monad 的文档应该会告诉您。您可以使用hoogle 搜索文档。例如,documentation of StateT 告诉您:

      return 函数保持状态不变,而&gt;&gt;= 使用第一个计算的最终状态作为第二个计算的初始状态。

      特定monad的实现

      如果您想了解有关特定 monad 实现的更多细节,您可能需要查看实际的实现。搜索 instance Monad ... 声明。例如,查看implementation of StateT。 list monad的实现在this file某处,搜索instance Monad []或者看看这个除了:

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

      也许不是最明显的定义,但如果您为列表单子调用 &gt;&gt;=,就会发生这种情况。

      总结

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

      【讨论】:

        【解决方案5】:

        仅通过查看它们的类型签名,您无法准确判断 return&gt;&gt;= 将为给定的 monad 做什么。那是因为他们的类型签名将总是

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

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

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

        【讨论】:

          【解决方案6】:

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

          【讨论】:

            【解决方案7】:

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

            类似地,bindreturn 和其他特定于 monad 的函数(如 State monad 中的 putget)只会让您了解 what 从一件事到另一件事的所有映射可以使用这些功能来完成。如果您需要了解 如何 monad 实际工作的底层逻辑,您可以查看文档(如果提供)或源代码。

            【讨论】:

            • 这里唯一的问题是查看类型签名我通常可以通过它的名称来判断函数的作用,在 monads 中它通常具有相同的“函数名称”(>>=)
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2020-02-06
            • 1970-01-01
            • 2018-09-19
            • 1970-01-01
            • 1970-01-01
            • 2011-07-23
            • 1970-01-01
            相关资源
            最近更新 更多