7

此代码(取自Learn You A Haskell):

main = do   putStr "Hey, "  
            putStr "I'm "  
            putStrLn "Andy!"  

显然脱糖

main =        putStr "Hey, " >>=  
       (\_ -> putStr "I'm "  >>= 
       (\_ -> putStrLn "Andy!"))

据我了解,它可以解释为“为了 putStrLn “Andy!”,我首先需要 putStr “I'm”,为了做到这一点,我首先需要 putStr “Hey,”;

我不同意这种解释,这很烦人,因为编译器显然没有,让我感到困惑。我遇到的问题是 lambdas 忽略了他们的论点,在惰性评估期间,这种事情不应该被识别和短路吗?

此外,当然,绑定返回一个 IO 操作,当该 IO 操作进入 main 时,它会被执行。但是如何阻止它打印“嘿,安迪!我是”?我怀疑这是绑定在做什么。

此外,“IO ()”类型的 IO 操作如何携带足够的信息以允许运行时系统打印“嘿,我是安迪!”?IO () 与 IO () 相比打印“Hello World!”有何不同?或写入文件?

考虑另一个,来自 monad 的维基百科页面:

加糖版:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

脱糖版本:

putStrLn "What is your name?" >>= 
   (\_ ->
      getLine >>=
         (\name ->
            putStrLn ("Nice to meet you, " ++ name ++ "!")))

类似的故事在这里。

我想我只需要查看 IO 绑定的定义,然后就一切都清楚了。如果有人可以帮助我逐步了解程序的实际评估方式并确定副作用发生的确切时刻,那么其他会很有帮助的事情。

4

5 回答 5

10

阅读西蒙·佩顿·琼斯 (Simon Peyton Jones) 撰写的“解决尴尬的小队”一文。

有关相关问题,请参阅

采取任何这样的解释,包括我的一粒盐——没有任何挥手可以代替严格的同行评审论文,而且这些解释必然是过度简化的。

一个非常粗略的观点是,>>=可以看作是一个列表构造函数:

data IO = [Primitive] 

和 IO 子系统解构该列表的值main并使用该列表。即“主is just a list. So you may want to take a look at the definition of Haskell entry point above,绑定”是相当无趣的。

您还可以阅读有关 haskell 历史的论文,并查看早期版本的 IO 子系统以了解正在发生的事情。

另请查看C 语言是Conal Elliott 的纯功能讽刺帖子。

功能纯度的定义很重要,我记得有一篇论文详细说明了这个定义,但我不记得标题了。

于 2011-11-22T11:21:09.047 回答
9

查看IO真正的 Haskell 实现可能会让人困惑,而不是启发。但是IO可以这样定义(假设您知道 GADT):

data IO a where
    Return a :: IO a
    Bind :: IO a -> (a -> IO b) -> IO b
    PutStr :: String -> IO ()
    GetLine :: IO String

instance Monad IO where
    return = Return
    (>>=) = Bind

putStr :: String -> IO ()
putStr = PutStr

getLine :: IO String
getLine = GetLine

因此,当您评估一个程序(类型IO ())时,它所做的只是构建一个类型的数据结构,IO ()该结构描述一旦您执行它,与世界的交互将如何发生。然后,您可以想象执行引擎是用例如 C 语言编写的,并且所有效果都发生在此处。

所以

main = do   putStr "Hey, "  
            putStr "I'm "  
            putStrLn "Andy!"  

是相同的

main = Bind (PutStr "Hey, ") (\ _ -> Bind (PutStr "I'm ") (\ _ -> PutStr "Andy!"))

这些排序来自执行引擎的工作方式。

也就是说,我知道没有真正以这种方式实现的 Haskell 实现。真正的实现倾向于实现IO为一个状态单子,带有一个表示正在传递的真实世界的令牌(这是保证排序的原因),而像这样的原语putStr只是对 C 函数的调用。

于 2011-11-22T12:07:50.473 回答
3

我想我只需要查看 IO 绑定的定义,然后就一切都清楚了。

是的,你应该这样做。这实际上很容易,如果我没记错的话,它就像

newtype IO = IO (RealWorld -> (a, RealWorld))

(IO f) >>= g = ioBind f g
    where
       ioBind :: (RealWorld -> (a, RealWorld)) -> (a -> IO b) -> RealWorld -> (b, RealWorld)
       ioBind f g rw = case f rw of
            (a, rw@RealWorld) -> case g a of
                IO b -> b rw

“诀窍”是每个 IO 值实际上基本上都是一个函数,但要评估它,您需要一个 type 的标记RealWorld。只有一个实例可以提供这样的值——运​​行 main 的运行时系统(当然还有不能命名的函数)。

于 2011-11-22T14:30:41.463 回答
1

如果您再次将操作视为功能,我认为这更容易理解。您的绑定示例 ( do { foo <- getLine ; putStrLn foo ; }) 在直观上类似于以下函数:

apply arg func = func (arg)

除了函数是事务。所以我们的调用func(arg)被评估,如果有的话,只有在(arg)成功完成的情况下。否则我们fail在我们的行动。

这与普通函数不同,因为 Haskell 真的不在乎(arg)计算是否完全或根本不计算,直到它需要一点点func(arg)才能继续程序。

于 2011-11-22T11:36:22.087 回答
1

我想我只需要看看bindfor的定义,IO然后一切就清楚了。

 -- ghc-8.6.5/libraries/base/GHC/Base.hs; line 1381
bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO (\ s -> case m s of (# new_s, a #) -> unIO (k a) new_s)

 -- ghc-8.6.5/libraries/base/GHC/Base.hs; line 1387
unIO :: IO a -> (State# RealWorld -> (# State# RealWorld, a #))
unIO (IO a) = a

IO类型在单独的模块中声明:

 -- ghc-8.6.5/libraries/ghc-prim/GHC/Types.hs; line 169
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

IO(更多关于and操作符的例子bind可以在Philip Wadler的How to Declare an Imperative中找到。)


如果有人可以帮助我逐步了解程序的实际评估方式并确定副作用发生的确切时刻,那么其他会很有帮助的事情。

让我们重写bindIO

  • 使用let-expressions 和bang-patterns而不是case

  • 使用以下方法从其第一个参数中提取操作unIO

    bindIO :: IO a -> (a -> IO b) -> IO b
    bindIO m k = IO (\ s -> let !(# new_s, a #) = unIO m s in unIO (k a) new_s)
    

现在对于那个Learn You A Haskell示例的扩展版本:

main =        putStr "Hey, " >>=  
       (\_ -> putStr "I'm "  >>= 
       (\_ -> putStrLn "Andy!"))
  1. 替换(>>=)bindIO, 沿途切换到前缀表示法:

    main = bindIO (putStr "Hey, ")
                  (\_ -> bindIO (putStr "I'm ")
                                (\_ -> putStrLn "Andy!"))
    
  2. 现在是挑剔的部分 - 将所有调用扩展到bindIO; 一切顺利,程序最终将类似于:

    main = IO (\s0 -> let !(# s1, _ #) = unIO (putStr "Hey, ") s0 in
                      let !(# s2, _ #) = unIO (putStr "I'm ") s1 in
                      unIO (putStrLn "Andy!") s2)
    
  3. 另一项更改 - 它是可选的,但它有助于澄清这里发生的事情:

    main = IO (\s0 -> let !(# s1, _ #)  = unIO (putStr "Hey, ") s0 in
                      let !(# s2, _ #)  = unIO (putStr "I'm ") s1 in
                      let !(# s3, a3 #) = unIO (putStrLn "Andy!") s2) in
                      (# s3, a3 #))
    
    • 其中,据我所知,它可以解释为:为了putStrLn "Andy!"我首先需要putStr "I'm ",为了做到这一点,我首先需要putStr "Hey, "

    正确:因为程序中如何使用 、 和s0s1一次)从而建立了评估顺序。该排序允许并直接使用效果来打印出它们各自的参数。s2s3putStrputStrLn

因此,与例如标准 ML(使用语法排序)不同,Haskell 依赖于数据依赖性来确保 I/O 以所需的顺序发生 -do符号只是一种方便。


我遇到的问题是 lambda 在惰性评估期间忽略了他们的论点 - 这种事情不应该被识别和短路吗?

如果我们还“过度扩展”您的另一个示例:

  IO (\ s0 -> let !(# s1, _ #)    = unIO (putStrLn "What is your name?") s0 in
              let !(# s2, name #) = unIO getLine s1 in
              let !(# s3, a3 #)   = unIO (putStrLn ("Nice to meet you, " ++ name ++ "!")) s2 in
              (# s3, a3 #))

我们可以清楚地看到,实际上被忽略的是输出- 即() :: ()来自使用putStrand putStrLn- 但不是states,它们保持顺序。


类型的 IO 操作如何IO ()携带足够的信息以允许运行时系统打印"Hey, I'm Andy!"- 这IO ()IO ()打印"Hello World!"或写入文件有什么不同?

相同的方式(+)(-)并且(*)被区别对待,即使它们具有相同的类型 ( Num a => a -> a -> a) - 通过具有不同的名称。

于 2020-06-20T12:30:27.977 回答