6

此代码(从Learn You A Haskell拍摄):懒惰的评价和IO副作用混乱

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

显然desugars到

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

其中,按照我的理解,可以interpretted的话说,“为了putStrLn“Andy!”我首先需要把STR“我是”,为了做到这一点,我首先需要把STR“嘿”;

我不同意这种解释,这是令人讨厌的,因为t他的编译器显然不会让我感到困惑。我对它的问题是,lambda忽略了他们的论点,在懒惰的评估中,这种事情不应该被认可并被短路?

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

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

考虑另一个,从维基百科页面单子:

加糖版本:

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的绑定的定义,然后它将全部清楚。如果有人能够帮助我逐步了解程序实际得到评估的方式并找出发生副作用的确切时刻,那么其他一些有用的东西就会有帮助。

+3

“拉姆达斯忽视他们的论点,在懒惰的评估过程中,这种事情不应该被认为是短暂的吗?”你打赌! '(>> =)'的第二个参数在这里是一个特别懒惰的函数,但是'(>> =)'函数_itself_不是懒惰的。 –

回答

10

阅读Simon Peyton Jones撰写的“Tackling the awkward squad”论文。

对于相关的问题,请参见

采取任何这样的解释,包括我的一粒盐 - 没有挥手可以取代严格的同行评议文件,并且解释必然过于简化。

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

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 language is purely functional Conal Elliott的讽刺文章。

功能纯度的定义并不重要,我记得一篇阐述定义的文章,但我不记得标题。

+0

有趣的是,每个人总是通过比喻解释>> = IO值。在前奏的某个地方没有定义吗?为什么没有人会引用它?在魔术发生的地方绑定了IO吗?我现在正在阅读那些笨拙的班级报告,甚至连SPJ都不愿意实际定义绑定 – TheIronKnuckle

+5

'IO'类型是抽象的,所以在Haskell标准中没有'>> ='的定义。根据你实现'IO'的方式,你会有'>> ='的不同实现。如果你深入了解ghc,你会发现'IO'是一个状态monad,'>> ='只是绑定状态monad。 (编译器中有更多的魔术来使这个效率更高。) – augustss

+0

@ThelronKnuckle它不是'值'的类比。这是使用旧的main :: [Request] - > [Response]'想法的另一个纯IO实现的类比。另外最有趣的部分不是绑定,而是IO monad的'runIO'。由于IO monad是抽象的,我们必须或者像SPJ那样提供抽象的解释,或者像我一样使用一些实现策略。 – nponeccop

1

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

apply arg func = func (arg) 

除了功能是事务。因此我们的电话func(arg)被评估,如果有任何只有(arg)成功完成。否则,我们fail在我们的行动。

这是不同于普通的功能,因为然后Haskell真的不在乎,如果(arg)完全计算或根本不在乎,直到它需要一点点func(arg)继续程序。

7

在一个真正的Haskell实现中看着IO可能会比其启发更多的混淆。不过想到IO像这样被定义的(假设你知道GADTs):

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作为状态monad,并带有表示真实世界被传递的令牌(这是保证排序的原因),而基本类型如putStr只是对C函数的调用。

+0

+1为GADT方法和'在真正的Haskell实现中查看IO可能会混淆更多的东西,而不是启发'的事情。 – nponeccop

3

我想我只需要看IO的绑定定义,然后它就会全部清楚。

是的,你应该这样做。这其实很简单,如果我remeber正确它是这样

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, [email protected]) -> case g a of 
       IO b -> b rw 

的“绝招”是每一个IO值实际上是基本功能,但要评价它,你将需要RealWorld类型的令牌。只有一个实例可以提供这样的值 - 运行main的运行时系统(当然还有不能命名的函数)。