Haskell "desugar" 如何在这个 do 块中获取行?

How does Haskell "desugar" getline in this do block?

我读过几本关于 Haskell 的书,但并没有编写太多代码,而且我对 Haskell 在某些情况下所做的事情感到有点困惑.假设我正在使用 getLine 这样用户可以按一个键继续,但我真的不想以任何有意义的方式解释那个人的输入。我相信这是一种有效的方法:

main = do
    _ <- getLine
    putStrLn "foo"

我明白这是做什么的基本要点。 getLine returns 一个 IO StringputStrLn 需要一个 String 和 returns IO (),所以如果我理论上想打印用户在控制台中输入的内容,我基本上会使用 Monad class 中的 >>= 运算符。就我而言,我相信我的代码等同于 getLine >> putStrLn "foo" 因为我丢弃了 getLine.

的 return 值

但是,如果我改为这样做呢?

main = do
    let _ = getLine
    putStrLn "foo"

在这种情况下,我们正在设置一种 lambda 来处理需要 IO String 的东西,对吧?我可以编写一个 printIOString 函数来打印用户的输入,这样就可以正常工作。但是,当我实际上没有使用 IO String 时,该程序的行为很奇怪...... getLine 甚至没有提示我输入;该程序只是打印出 "foo".

我不太确定这里的“脱糖”语法是什么,或者这是否会阐明 Haskell 在幕后所做的事情。

第一个案例如您所料脱糖:

main = getLine >>= \_ -> putStrLn "foo"

相当于

main = getLine >> putStrLn "foo"

第二种情况,

main = do
    let _ = getLine
    putStrLn "foo"

脱糖为

main = let _ = getLine in putStrLn "foo"

由于计算 let 表达式的 RHS 不需要 _ = getLine 值,编译器可以随意忽略它并且永远不会执行 IO 效果,这就是为什么你不再提示输入 CLI。

尽管两种情况都忽略了 getLine 的结果,但区别在于第一种情况在 IO 上下文中评估 getLine,而第二种情况将 getLine 评估为一个纯粹的价值。在 IO 中,side-effects 必须一起执行和排序,但在纯上下文中,编译器可以自由地忽略未使用的值。

我不推荐这样做,因为它不是很地道,但你可以写成类似

的东西
printIOString :: IO String -> IO ()
printIOString ios = ios >>= putStrLn

并像 printIOString getLine

一样使用它

让我们用几个更复杂的例子来热身。

main = do
    x
    x
    x
    putStrLn "foo"
    where
    x = do
        getLine

您希望它做什么?我不了解你,但是 I 期望程序得到三行然后打印一些东西。如果我们对第二个 do 块进行脱糖,我们会得到

main = do
    x
    x
    x
    putStrLn "foo"
    where x = getLine

因为这是另一个的脱糖,它的行为是一样的,在打印前得到三行。如果您觉得第一个不直观,还有另一种思路可以得出相同的答案。 “引用透明性”,Haskell 的定义特征之一,确切地说,你可以用它的定义替换对某物(即变量名)的“引用”,所以前面的程序应该是完全一样的编程为

main = do
    getLine
    getLine
    getLine
    putStrLn "foo"

如果我们认真对待方程 x = getLine。好的,所以我们有一个读取三行并打印的程序。这个怎么样?

main = do
    x
    x
    putStrLn "foo"
    where x = getLine

获取两行并打印。还有这个?

main = do
    x
    putStrLn "foo"
    where x = getLine

获取一行,然后打印。希望你看到这是怎么回事...

main = do
    putStrLn "foo"
    where x = getLine

获取零行然后打印,即立即打印!我使用 where 而不是 let 来使开头的示例更加明显,但是您几乎总是可以用它的 let 表亲替换 where 块而不改变其含义:

main = let x = getLine in do
    putStrLn "foo"

因为我们不引用 x,我们甚至不需要命名它:

main = let _ = getLine in do
    putStrLn "foo"

这是你写的代码的脱糖。

根据https://whosebug.com/tags/do-notation/info,

do { let { _ = getLine } ; putStrLn "foo" } 
= 
do { let { _ = getLine } in putStrLn "foo" } 
= 
     let { _ = getLine } in putStrLn "foo"

Haskell 语义等同于

    getLine & (\ _ -> putStrLn "foo")
=
                      putStrLn "foo"

(与 x & f = f x),而实际上

do { _ <- getLine ; putStrLn "foo" }
=
  getLine >>= (\ _ -> putStrLn "foo")

无法进一步简化。