【问题标题】:What does it mean to "run" inside a monad?在 monad 中“运行”是什么意思?
【发布时间】:2020-08-31 23:43:24
【问题描述】:

阅读有关不同 monad 的 Haskell 教科书章节,当作者从解释 bind 的细节和 monad 法则跳到实际使用 monad 时,我反复迷失了方向。突然,诸如“在 monadic 上下文中运行函数”或“运行 monad”之类的表达出现了。同样,在库文档和关于 monad 转换器堆栈的讨论中,我读到一些函数“可以在任何选择的 monad 中运行”的声明。这个“在 monad 中运行”到底是什么意思?

有两件事我似乎没有弄清楚:

  1. monad 是一个具有函数(return>>=)和定律的类型类。因此,在 monad 中“运行”某些东西可能意味着 (a) 将其作为参数提供给 return,或者 (b) 使用 >>= 对其进行排序。如果 monad 是 m a 类型,那么在情况 a) 中,something 必须是 a 类型,以匹配 return 函数的类型。如果 b) something 必须是 a -> m b 类型的函数,以匹配 >>= 函数的类型。由此,我不明白如何在任意 monad 中“运行”某些函数,因为我使用 >>= 排序的函数都必须具有相同的类型签名,并且我使用 return 提升的值必须是特定的 monad 类型参数。
  2. 据我了解,在函数式语言中没有执行运行计算的概念 - 只有对某个参数的函数应用,并评估函数 (用它的值替换它)。然而,许多特定的 monad 带有 run 函数,例如 runReaderrunState 等。这些函数不是 monad 定义的一部分,它们是普通函数,在任何情况下都不是特殊的命令式语句语言的功能核心。那么,它们“运行”的是什么?

我觉得清楚地理解这些概念是理解 monad 转换器堆栈或类似结构的关键,这些结构似乎是理解 Haskell 中任何实质性库和任何重要程序所必需的。非常感谢您帮助我实现了从简单编写函数式代码到真正理解其含义的飞跃。

【问题讨论】:

    标签: haskell monads monad-transformers purely-functional


    【解决方案1】:

    撰写书籍和文章的作者在尝试解释概念时经常使用隐喻和不太精确的语言。目的是让读者对正在发生的事情有一个概念上的直觉。

    我相信“运行”函数的概念属于这一类。除了IO,你是对的,你用来组合的函数,比如[]Maybe等等,与其他函数没有什么特别的。

    我认为,在 monad 中运行某些东西的概念来自functors are containers 的观察。这个观察也适用于单子,因为所有单子都是函子。 [Bool] 是布尔值的容器,Maybe Int 是(零或一)数字的容器。您甚至可以将阅读器函子 r -> a 视为 a 值的容器,因为您可以想象它只是一个非常大的查找表。

    能够“在容器内运行函数”很有用,因为并非所有容器都允许访问其内容。同样,IO 是最好的例子,因为它是一个不透明的容器。

    一个常见问题是:How to return a pure value from a impure method。同样,许多初学者会问:我如何获得Maybe 的值?你甚至可以问:我如何从列表中获取价值?概括地说,问题变成:How to get the value out of the monad

    答案是你不知道。你'在容器内运行函数',或者,正如我喜欢说的,你inject the behaviour into the monad。你永远不会离开容器,而是让你的函数在它的上下文中执行。特别是对于IO,这是您可以与该容器交互的唯一方式,因为否则它是不透明的(我在这里假装unsafePerformIO 不存在)。

    请记住,当涉及到 bind 方法 (>>=) 时,虽然“在其中运行”的函数具有 a -> m b 类型,但您也可以“运行”a ' normal' 函数 a -> b 在带有 fmap 的 monad 中,因为所有 Monad 实例也是 Functor 实例。

    【讨论】:

    • 如何从 monad 中获取值:在我的理解中,这不是关于 monad 本身的问题,而是如果模块,其中具有 monad 类型实例的类型类是否已定义,是否公开类型数据构造函数。如果是这样,我可以对其进行模式匹配并提取包装在该类型中的值。当使用>>= 时,我不需要那种模式匹配,因为它是在>>= 中隐式完成的。
    • @UlrichSchuster 确实这不是单子 抽象 的问题,而是特定单子的问题(同样,值得注意的是:IO)。不过,这不是数据构造函数是否被导出的问题。 []MaybeEither e 都导出它们的数据构造函数。您如何从其中的任何一个中获得 价值?
    • 嗯,不一定只有一个值,正如您在博客文章 blog.ploeh.dk/2019/02/04/how-to-get-the-value-out-of-the-monad 中指出的那样。但只要我可以在数据构造函数上进行模式匹配,我就可以搞乱类型的实现。这就是为什么我还试图更好地理解 Haskell 中的抽象数据类型 (stackoverflow.com/questions/61638297/…)。
    • @UlrichSchuster 是的,您当然可以在[]MaybeEither e 等的数据构造函数上进行模式匹配,但是您必须通过手动编写代码来涵盖所有情况处理每种情况。 fmap>>= 为您提供了在相关容器中组合较小函数的便捷方法。这对于Either e 可能特别清楚,当您想专注于代码的快乐路径时,您可以忽略Left 的情况。
    【解决方案2】:

    我使用 >>= 排序的函数必须都具有相同的类型签名

    这只是一个事实。在某些单子上下文中,我们可能会有这样的表达方式

    x >>= f >>= g
    

    在哪里

    x :: Maybe Int
    f :: Int -> Maybe String
    g :: String -> Maybe Char
    

    所有这些都必须涉及相同的 monad(也许),但请注意,它们并非都具有相同的类型签名。与普通函数组合一样,您不需要所有返回类型都相同,只需一个函数的输入与其前一个函数的输出匹配即可。

    【讨论】:

      【解决方案3】:

      这里是“在容器内运行函数”的简单类比,带有伪代码:

      假设你有一个 Future[String] 类型,它代表一个容器,该容器将在“未来某个时间”有一个字符串:

      val tweet: Future[String] = getTweet()
      

      现在你想访问字符串——但你不要把字符串从上下文中取出——“未来”——你只需使用字符串“在容器内”:

      tweet.map { str =>
        println(str)
      }
      

      在这些花括号里,你是“未来的”。例如:

      val tweet: Future[String] = getTweet()
      
      tweet.map { str =>
        println(str)
      }
      
      println("Length of tweet string is " + tweet.length) // <== WRONG -- you are not yet in the future
      

      tweet.length 正在尝试访问容器外的推文。因此,在阅读源代码时,“在容器内”类似于“在地图(平面地图等)的花括号内”。你正在浸入容器内。

      tweet.map { str =>
        println("Length of tweet string is " + str.length) // <== RIGHT
      }
      

      虽然是一个非常简单的类比,但在考虑所有单子时,我发现这很有用。在源代码中,“容器内”在哪里,“容器外”在哪里?在这种情况下,长度函数在未来运行,或者“在容器内”。

      【讨论】:

        猜你喜欢
        • 2020-06-14
        • 2011-08-12
        • 2017-06-11
        • 2018-03-05
        • 2023-03-27
        • 2011-10-12
        • 2013-05-07
        • 2016-08-17
        • 2010-12-28
        相关资源
        最近更新 更多