Haskell 惰性评估和IO副作用混淆

Haskell 惰性评估和IO副作用混淆,haskell,functional-programming,lazy-evaluation,side-effects,Haskell,Functional Programming,Lazy Evaluation,Side Effects,此代码(取自): 很明显,他对我很感兴趣 main = putStr "Hey, " >>= (\_ -> putStr "I'm " >>= (\_ -> putStrLn "Andy!")) 据我所知,这句话可以解释为“为了让安迪”安迪!我首先需要输入“我是”,为了做到这一点,我首先需要输入“嘿,” 我不同意这种解释,这很烦人,因为编译器显然没有这样做,让我感到困惑。我的问题是lambda忽略了它们的参数

此代码(取自):

很明显,他对我很感兴趣

main =        putStr "Hey, " >>=  
       (\_ -> putStr "I'm "  >>= 
       (\_ -> putStrLn "Andy!"))
据我所知,这句话可以解释为“为了让安迪”安迪!我首先需要输入“我是”,为了做到这一点,我首先需要输入“嘿,”

我不同意这种解释,这很烦人,因为编译器显然没有这样做,让我感到困惑。我的问题是lambda忽略了它们的参数,在惰性求值期间,这种事情不应该被识别和短路吗

当然,绑定也会返回一个IO操作,当该IO操作落入main时,它会被执行。但是如何阻止它打印“嘿,Andy!我是?,我怀疑是bind在做什么

另外,类型为“IO()”的IO操作如何携带足够的信息以允许运行时系统打印“嘿,我是安迪!“?IO()与打印“你好,世界!”!“还是写入文件

另一个例子是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 ++ "!")))
这里也有类似的故事

我想我只需要看看bind for IO的定义,然后它就会清楚了。如果有人能帮助我逐步了解程序是如何评估的,并确定副作用发生的确切时刻,那将非常有帮助。

阅读西蒙·佩顿·琼斯的“”论文

有关相关问题,请参阅

  • ,
以任何这样的解释为例,包括我的解释——没有任何一种挥手的方式可以取代严格的同行评议论文,而且这些解释必然过于简单化

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

data IO = [Primitive] 
IO子系统解构
main
的值并使用该列表。也就是说,`main
只是一个列表。因此,您可能想看看上面的Haskell入口点的定义,
main
bind`相当无趣

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

还可以看看康纳尔·艾略特的讽刺文章


函数纯度的定义是非常重要的,我记得有一篇文章详细阐述了这个定义,但我不记得标题。

我认为如果你把这些动作重新看作函数,这会更容易理解。你的绑定示例(
do{foo在一个真正的Haskell实现中查看
IO
可能会比它所启发的更让人困惑。但是可以这样定义
IO
(假设您了解GADT):

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

所以

main = Bind (PutStr "Hey, ") (\ _ -> Bind (PutStr "I'm ") (\ _ -> PutStr "Andy!"))
它们的顺序来自于执行引擎的工作方式

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

我想我只需要看看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值实际上基本上都是一个函数,但要对其求值,您需要一个类型为
RealWorld
的标记。只有一个实例可以提供这样的值—运行main的运行时系统(当然,还有不能命名的函数)

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

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

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

Philip Wadler提供的
IO
bind
操作符的更多示例,包括标准ML的定义(第3.4节,第24页).

有趣的是,每个人都会通过类比来解释>>=关于IO值。难道前奏曲中没有定义吗?为什么没有人引用它?是bind over IO魔术发生的地方吗?我现在正在阅读令人尴尬的队报,甚至连SPJ都不敢真正定义bind。
IO
类型是抽象的,所以Haskell标准中没有对
>=
的定义。根据您实现
IO
的方式,您将有不同的
>=
实现。如果深入研究ghc,您会发现
IO
是一个状态单子,而
>=
只是为状态单子绑定。(编译器内部有更多的魔法来提高效率。)@ThellonKnuckle它不是对“值”的类比。它是对另一个纯IO实现的类比,使用旧的
main::[Request]->[Response]
idea。另外,最有趣的部分不是绑定,而是IO monad的
runIO
。由于IO monad是抽象的,我们必须像SPJ那样提供抽象的解释,或者像我一样使用一些实现策略。+1对于GADT方法和“在真实的Haskell实现中查看IO”可能会让更多人困惑n它启发了‘东西’。“lambda忽略了它们的参数,在惰性求值期间,这种事情不应该被识别和短路吗?”当然!
(>>=)
的第二个参数在这里是一个特别懒惰的函数,但是
(>>=)
函数本身并不懒惰。
main = do   putStr "Hey, "  
            putStr "I'm "  
            putStrLn "Andy!"  
main = Bind (PutStr "Hey, ") (\ _ -> Bind (PutStr "I'm ") (\ _ -> PutStr "Andy!"))
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
 -- 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
 -- ghc-8.6.5/libraries/ghc-prim/GHC/Types.hs; line 169
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))