如何快速阅读 do 符号而不转换为 >>= 组合?

Luc*_*lla 0 monads haskell functional-programming do-notation

这个问题与这篇文章有关:Understanding do notation for simple Reader monad: a <- (*2), b <- (+10), return (a+b)

我不在乎一种语言是否难以理解,如果它有望解决一些易于理解的语言给我们带来的问题。我被承诺在 Haskell(和其他函数式语言)中不可能改变状态是一个游戏规则改变者,我确实相信这一点。我的代码中有太多与状态相关的错误,我完全同意这篇文章,即在 OOP 语言中对对象的交互进行推理几乎是不可能的,因为它们可以改变状态,因此为了推理代码,我们应该考虑这些状态的所有可能排列。

但是,我发现对 Haskell monad 的推理也非常困难。正如您在我链接的问题的答案中所见,我们需要一个大图来理解 do 符号的 3 行。>>=为了理解代码,我总是打开stackit.io手动去糖化do符号并一步一步地编写do符号的应用程序。

这个问题或多或少是这样的:在大多数情况下,当我们有S a >>= f我们必须展开aS并应用f它。然而,f实际上在形式中或多或少是另一回事S a >>= g,我们也必须解开等等。人脑不是那样工作的,我们不能轻易地把这些东西应用到头脑中然后停下来,把它们放在大脑的堆栈中,然后继续应用剩下的东西,>>=直到我们到达终点。当到达终点时,我们将所有这些东西存储在大脑的堆栈中并将它们粘合在一起。

因此,我一定是做错了什么。必须有一种简单的方法来理解>>=大脑中的“成分”。我知道 do 表示法非常简单,但我只能将其视为一种轻松编写>>=作文的方法。当我看到do符号时,我只是将它翻译成一堆 >>=. 我不认为它是理解代码的一种单独方式。如果有办法,我希望有人告诉我。

所以问题是:如何阅读do符号?

Fyo*_*kin 5

第 1 部分:无需深入研究

实际上,monad 背后有一个非常简单、易于掌握的直觉:它们对事情发生的顺序进行编码。比如,先做这件事,然后做另一件事,然后做第三件事。例如:

executeMadDoctrine = do
    wait oneYear
    s <- evaluatePoliticalSituation
    case s of
        Stable -> do
            printInNewspapers "We're going to live another day"
            executeMadDoctrine -- recursive call
        Unstable -> do
            printInNewspapers "Run for your lives"
            launchMissiles
            return ()
Run Code Online (Sandbox Code Playgroud)

或者一个更现实的(也是可编译和可执行的)示例:

main = do
    putStrLn "What's your name?"
    name <- getLine
    if name == "EXIT" then
        return ()
    else do
        putStrLn $ "Hi, " <> name
        main
Run Code Online (Sandbox Code Playgroud)

简单的。就像Python一样。确实,人脑确实这样工作的。

你看,除非你开始做更高级的事情,否则你不需要知道它内部是如何工作的。毕竟,您每次启动汽车时可能都没有考虑气缸点火的顺序,对吗?你只要踩油门就可以了。这与do.

第 2 部分:你选择了一个坏例子

您在上一个问题中选择的示例不是此内容的最佳候选者。Monad函数的实例确实有点伤脑筋。甚至我也必须花一点力气去理解发生了什么——而且我已经专业地做 Haskell 有一段时间了。

这里的问题是数学。这该死的事情一次又一次地证明是不合理的,尤其是在没有人要求的情况下。

想一想:首先,我们有非常好的自然数,我们可以很好地理解。我有两只眼睛,你有一把剑,我还是跑吧。但后来证明我们需要零。为什么我们需要它?这是亵渎!你不能写下不是的东西!但事实证明你必须拥有它。它毫不含糊地从我们知道是真实的其他东西中得出。然后我们得到了无理数。是WTF吗?我怎么理解呢?我不能吗?毕竟是橘子,我可以吗?但它们也必须存在。它只是跟随。没办法。然后是复数,超验的,超复杂的,无法构造的......我的大脑在这一点上沸腾了。

这与 monad 有点相似:有一个特殊的数学对象,在某些时候有人注意到它非常适合表达计算顺序,因此我们为此使用了 monad。但后来事实证明,从数学上讲,各种事物都可以看起来像 monad。没办法,就是这样。

所以我们有所有这些有趣的例子。和do符号仍然为他们工作,因为他们的单子(从数学上讲),但它不再是命令。比如,你知道列表也是 monad 吗?但就像函数一样,列表的解释不是“顺序”,而是嵌套循环。如果你将列表与其他东西结合起来,你会得到不确定性。好玩的东西。

但就像处理不同类型的数字一样,您可以学习。随着时间的推移,你可以建立直觉。你绝对有必要吗?见第 1 部分。


che*_*ner 5

给定一个简单的代码,例如

foo :: Monad m => m Int -> m Int -> m Int
foo x y = do
    a <- y  -- I'm intentionally doing y first; see the Either example
    b <- x
    return (a + b)
Run Code Online (Sandbox Code Playgroud)

<-除了它从or “获取”一个Int值之外,你不能说太多。“得到”的含义很大程度上取决于它是什么。xym


一些例子:

米〜也许

foo (Just 3) (Just 5)评估为Just 8; 将任一参数替换为Nothing, 即可得到Nothing<-尝试从值中获取值Maybe Int,但如果失败则中止块的其余部分。

m ~ 要么

与 几乎相同Maybe,但替换Nothing为它遇到的第一个Left值。foo (Right 3) (Right 5)返回Right 8foo x (Left "foo")返回Left "foo",无论x是 aRight还是Left值。

米~[]

现在,不再获取 Int而是<-获取给定选项中的每个选项。 Int它这样做是不确定的;您可以想象该函数“分叉”成多个并行副本,每个副本都从其列表中选择了不同的值。最后,最终结果是所有计算结果的列表。

foo [1,2] [3,4]返回[4, 5, 5, 6]( [3 + 1, 3 + 2, 4 + 1, 4 + 2])。

米~IO

这个很棘手,因为与我们之前看过的 monad 不同,不一定有值需要获取。foo readLn readLn将返回从标准输入读取的两个数字的总和,如果读取的字符串不可解析为Int值,则可能会出现运行时错误。

您可能会认为它像Maybemonad 一样工作,但用运行时异常替换Nothing.