Haskell中的monadic IO构造只是一个约定吗?

Haskell中的monadic IO构造只是一个约定,还是有一个实现的原因呢?

你不只是FFI到libc.so而是做你的IO,并跳过IO Monad块?

无论如何它会起作用,或者由于Haskell评估懒惰或其他原因,结果是不确定的,比如GHC是IO Monad的模式匹配,然后以特殊方式或其他方式处理它。

什么是真正的原因? 最后你会产生副作用。 那为什么不用简单的方法呢?

是的,单点I / O是Haskell懒惰的结果。 具体而言,单点I / O是Haskell 纯粹的结果 ,这对懒惰语言是可预测的是非常有必要的。

举例来说,这很容易说明。 试想一下,Haskell 不是纯粹的,但仍然是懒惰的。 而不是types为String -> IO ()putStrLn ,它只是简单地使用String -> ()types,并且会将一个string作为一个副作用输出到stdout。 这样做的麻烦在于,只有在putStrLn被调用的时候才会发生这种情况,而在懒惰的语言中,只有在需要结果时才会调用函数。

这是麻烦: putStrLn产生() 。 查看types()的值是无用的,因为()意味着“无聊” 。 这意味着这个程序会做你所期望的:

 main :: () main = case putStr "Hello, " of () -> putStrLn " world!" -- prints “Hello, world!\n” 

但是我认为你可以同意编程风格很奇怪。 然而,这种case ... of是必要的,因为它通过匹配()putStrputStr的调用进行评估。 如果你稍微调整了一下程序:

 main :: () main = case putStr "Hello, " of _ -> putStrLn " world!" 

…现在它只打印world!\n ,而第一个电话是根本不评估。

然而,这实际上变得更糟了,因为一旦你开始尝试做任何实际的编程,它就变得更难预测。 考虑这个程序:

 printAndAdd :: String -> Integer -> Integer -> Integer printAndAdd msg xy = putStrLn msg `seq` (x + y) main :: () main = let x = printAndAdd "first" 1 2 y = printAndAdd "second" 3 4 in (y + x) `seq` () 

这个程序是否打印出first\nsecond\nsecond\nfirst\n ? 不知道(+)评估其论点的顺序, 我们不知道 。 而在Haskell中,评估顺序并不总是定义良好,所以完全可能两个效果的执行顺序实际上完全不可能确定!

这个问题在严格定义的评估顺序中没有出现严格的语言,但是像Haskell这样的懒惰语言,我们需要一些额外的结构来确保副作用是(a)实际评估和(b)以正确的顺序执行。 Monad碰巧是一个界面,优雅地提供必要的结构来执行该命令。

这是为什么? 那又如何呢? 那么, monadic接口在>>=的签名中提供了一个数据依赖的概念 ,它实施了一个定义明确的评估顺序。 Haskell的IO的实现是“魔术”,就是说它在运行时被实现,但是monadic接口的select远不是任意的。 这似乎是一种用纯语言编码顺序操作概念的相当好的方法,它使得Haskell可以在不牺牲可预测的效果顺序的情况下变得懒惰和透明地透明。

值得注意的是monad并不是以纯粹的方式对副作用进行编码的唯一方式 – 事实上,历史上, 它们甚至不是Haskell处理副作用的唯一方法 。 不要误以为monad只是I / O(他们不是),只用于懒惰的语言(即使用严格的语言,它们对于保持纯度也是非常有用的),只用于纯语言(许多事情是有用的单子,不仅仅是为了强制纯度),或者你需要单子做I / O(你不这样做)。 尽pipe如此,他们在Haskell中似乎也做得非常好。


†关于这一点,西蒙·佩顿·琼斯(Simon Peyton Jones)曾经指出: “懒惰使你保持诚实” 。

你可以只是FFI到libc.so,而不是做IO,跳过IO Monad的东西?

https://en.wikibooks.org/wiki/Haskell/FFI#Impure_C_Functions取得; ,如果您将FFI函数声明为纯粹的(所以不需要引用IO),那么

GHC在计算两次纯函数结果时看不出任何意义

这意味着函数调用的结果被有效地caching。 例如,一个程序,其中一个外国不纯的伪随机数发生器被声明为返回一个CUInt

 {-# LANGUAGE ForeignFunctionInterface #-} import Foreign import Foreign.C.Types foreign import ccall unsafe "stdlib.h rand" c_rand :: CUInt main = putStrLn (show c_rand) >> putStrLn (show c_rand) 

每次调用都会返回相同的结果,至less在我的编译器/系统上:

 16807 16807 

如果我们改变声明返回一个IO CUInt

 {-# LANGUAGE ForeignFunctionInterface #-} import Foreign import Foreign.C.Types foreign import ccall unsafe "stdlib.h rand" c_rand :: IO CUInt main = c_rand >>= putStrLn . show >> c_rand >>= putStrLn . show 

那么这会导致(可能)不同的数字返回每个调用,因为编译器知道它是不纯的:

 16807 282475249 

所以你不得不使用IO来调用标准库。

比方说使用FFI我们定义了一个函数

 c_write :: String -> () 

这是纯粹的谎言,因为每当它的结果被迫打印string。 为了不在Michal的回答中遇到caching问题,我们可以定义这些函数来获取额外的()参数。

 c_write :: String -> () -> () c_rand :: () -> CUInt 

在一个实现级别上,只要CSE不太积极(它不在GHC中,因为这会导致意外的内存泄漏),就会工作。 现在我们已经有了这样的定义,Alexis指出了许多令人尴尬的用法问题 – 但是我们可以用monad来解决它们:

 newtype IO a = IO { runIO :: () -> a } instance Monad IO where return = IO . const m >>= f = IO $ \() -> let x = runIO m () in x `seq` fx rand :: IO CUInt rand = IO c_rand 

基本上,我们只是把Alexis的所有尴尬的用法问题塞进monad中,只要我们使用monadic接口,一切都保持可预测性。 从这个意义上说, IO只是一个约定 – 因为我们可以在Haskell中实现它,没有什么基本的东西。

这是从经营的有利位置。

另一方面,报告中的Haskell的语义是单独使用指称语义指定的。 而且,在我看来,Haskell具有精确的指称语义的事实是语言中最美丽和最有用的特质之一,它使我有一个精确的框架来思考抽象,从而精确地pipe理复杂性。 虽然通常的抽象IO monad没有接受的指称语义( 对于我们中的一些人的哀叹 ),但至less可以想象,我们可以为它创build一个指示模型,从而保留了Haskell指示模型的一些好处。 然而,我们刚才给出的I / Oforms完全不符合Haskell的指称语义。

简单地说,只能有两个可区分的值(模致命错误消息)types() :( ()和⊥。 如果我们把FFI作为I / O的基本原理,并且只把IO monad作为一个约定来使用,那么我们就有效地给每一个types增加了十亿个值 – 继续有一个指称语义,每个值必须与在评估之前进行I / O操作,而且由于引入了额外的复杂性,除了最微不足道的情况之外,我们基本上失去了考虑任何两个截然不同的程序的能力 – 也就是说,我们失去了重构的能力。

当然,由于unsafePerformIO这在技术上已经是这种情况了,高级Haskell程序员也需要考虑操作语义。 但是大多数情况下,包括使用I / O的时候,我们可以忘掉所有这些并且自信地重构,正因为我们已经知道当我们使用unsafePerformIO ,我们必须非常小心地确保它很好的运行,给予我们尽可能多的指称推理。 如果一个函数具有unsafePerformIO ,我会自动给它比常规函数多5到10倍的注意力,因为我需要了解有效的使用模式(通常types签名告诉我所有我需要知道的),我需要考虑caching和种族的条件,我需要考虑我需要多深的力量,结果等等。这很糟糕[1]。 FFI I / O同样需要注意。

总而言之:是的,这是一个惯例,但是如果你不遵循这个惯例,那么我们就不会有好的东西。

[1]其实我觉得这很有趣,但是总是考虑到所有这些复杂性是不切实际的。

这取决于“是”的含义是什么,或者至less“约定”的含义是什么。

如果一个“惯例”是指“通常做的事情的方式”或“涉及特定事务的各方之间的协议”,那么很容易给出一个无聊的答案:是的, IO monad是一个惯例。 语言devise者同意处理IO操作的方式以及语言用户通常执行IO操作的方式。

如果允许我们select一个更有趣的“约定”的定义,那么我们可以得到一个更有趣的答案。 如果一个“惯例”是用户为了在没有语言本身的帮助的情况下实现一个特定的目标而强加给他的语言的话,那么答案是否定的: IO monad是一个惯例的反义词 。 它是一门由语言执行的学科,协助用户构build和推理程序。

IOtypes的目的是在“纯”值的types和需要由运行时系统执行以产生有意义的结果的值的types之间创build明确的区分。 Haskelltypes的系统强制执行这种严格的分离,防止用户(比如说)创build一个Inttypes的值来启动谚语导弹。 这不是第二种意义上的惯例:它的整个目标是将用于执行副作用所需的训练以一种安全一致的方式用户转移到语言和编译器上。

你可以只是FFI到libc.so,而不是做IO,跳过IO Monad的东西?

当然,可以在没有IO monad的情况下执行IO:几乎可以看到所有其他现存的编程语言。

无论如何它会起作用,或者是因为Haskell评估懒惰或其他东西而导致的不确定性,比如GHC是IO Monad的模式匹配,然后以特殊方式或其他方式处理它。

没有免费的午餐。 如果Haskell允许任何值需要涉及IO的执行,那么它将不得不失去我们所看重的其他东西。 其中最重要的可能是参考透明度 :如果myInt有时是1 ,有时是5取决于外部因素,那么我们将失去大多数以严格的方式推理我们的程序的能力(称为等式推理 )。

在其他答案中提到了懒惰,但懒惰的问题将特别是共享将不再安全。 如果let x = someExpensiveComputationOf y in x * x中不是透明的,那么GHC将不能共享这个工作,并且将不得不计算两次。

什么是真正的原因?

如果没有从IO提供的有效值和编译器强制执行的有效值的严格分离,Haskell将会有效地停止成为Haskell。 有很多语言不执行这个规定。 至less有一个这样做会很好。

最后,你终于结束了一个副作用。 那为什么不用简单的方法呢?

是的,最后你的程序用一个名为main的值来表示一个IOtypes。 但问题并不在于你最终的结果,而是你开始的地方:如果你能够以严谨的方式区分有效和无效的价值观,那么在构build这个程序的时候就会获得很多优势。