Haskell中的monadic IO构造只是一个约定还是存在实现原因?
你能不能只用FFI进入libc.so而不是你的IO,并跳过IO Monad组件?
无论如何都会起作用,或者结果是不确定的,因为Haskell评估懒惰或其他东西,比如GHC是IO Monad的模式匹配,然后以特殊方式或其他方式处理它。
真正的原因是什么?最后你最终会产生副作用。那么为什么不以简单的方式去做呢?
答案 0 :(得分:69)
是的,monadic I / O是Haskell懒惰的结果。但具体来说,monadic I / O是Haskell pure 的结果,这对于懒惰语言是可预测的有效是必要的。†
这很容易用一个例子来说明。想象一下,Haskell 不纯净,但它仍然是懒惰的。它不是putStrLn
类型为String -> IO ()
,而是只有String -> ()
类型,它会将字符串打印到stdout作为副作用。这样做的麻烦在于,这只会在实际调用putStrLn
时发生,而在惰性语言中,仅在需要结果时调用函数。
问题在于:putStrLn
生成()
。查看()
类型的值是无用的,因为()
means “boring”。这意味着这个程序可以达到您的期望:
main :: ()
main =
case putStr "Hello, " of
() -> putStrLn " world!"
-- prints “Hello, world!\n”
但我认为你可以同意编程风格很奇怪。但是,case ... of
是必要的,因为它强制通过与putStr
匹配来评估对()
的调用。如果你稍微调整程序:
main :: ()
main =
case putStr "Hello, " of
_ -> putStrLn " world!"
...现在它只打印world!\n
,并且根本不会评估第一个调用。
但实际情况会变得更糟,因为一旦你开始尝试进行任何实际的编程,它就会变得更难以预测更难。考虑一下这个程序:
printAndAdd :: String -> Integer -> Integer -> Integer
printAndAdd msg x y = putStrLn msg `seq` (x + y)
main :: ()
main =
let x = printAndAdd "first" 1 2
y = printAndAdd "second" 3 4
in (y + x) `seq` ()
此程序是否打印出first\nsecond\n
或second\nfirst\n
?在不知道(+)
评估其参数的顺序的情况下,我们不知道。在Haskell中,评估顺序甚至不总是很明确,因此完全有可能实现两个效果的顺序实际上完全无法确定!
这个问题不会出现在具有明确定义的评估顺序的严格语言中,但在像Haskell这样的惰性语言中,我们需要一些额外的结构来确保副作用是(a)实际评估和(b)执行正确的订单。 Monads碰巧是一个优雅地提供必要结构来强制执行该命令的接口。
为什么?那怎么可能呢?好吧,the monadic interface provides a notion of data dependency in the signature for >>=
,它强制执行明确定义的评估顺序。 Haskell的IO
实现是“神奇的”,因为它是在运行时实现的,但是monadic接口的选择远非任意。这似乎是用纯语言编码顺序动作概念的一种相当好的方法,它使得Haskell可以在不牺牲可预测的效果排序的情况下保持懒惰和引用透明。
值得注意的是,monad不是唯一方式以纯粹的方式编码副作用 - 事实上,历史上,they’re not even the only way Haskell handled side-effects。不要误以为monad只用于I / O(它们不是),只在懒惰的语言中有用(即使用严格的语言它们对于保持纯度也很有用),只能用于纯语言(很多东西都是有用的monad,不仅仅是为了强制执行纯度),或者你需要monad来做I / O(你没有)。但是,出于这些目的,他们似乎在Haskell中做得非常好。
†关于此,Simon Peyton Jones曾指出“Laziness keeps you honest” with respect to purity。
答案 1 :(得分:24)
你是否可以将FFI转换为libc.so而不是执行IO并跳过IO Monad的事情?
从https://en.wikibooks.org/wiki/Haskell/FFI#Impure_C_Functions获取,如果您将FFI函数声明为纯函数(因此,没有引用IO),那么
GHC认为计算纯函数结果两倍没有意义
表示有效缓存函数调用的结果。例如,声明外部不纯伪随机数生成器返回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)
每次调用返回相同的内容,至少在我的编译器/系统上:
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来调用标准库。
答案 2 :(得分:12)
让我们说使用FFI我们定义了一个函数
c_write :: String -> ()
这取决于它的纯度,因为每当它的结果被强制它打印字符串。因此,我们不会在Michal的答案中遇到缓存问题,我们可以定义这些函数以获取额外的()
参数。
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` f x
rand :: IO CUInt
rand = IO c_rand
基本上,我们只是将所有Alexis的尴尬用法问题都填入monad中,只要我们使用monadic界面,一切都保持可预测。在这个意义上,IO
只是一个约定 - 因为我们可以在Haskell中实现它,没有任何基本的东西。
从操作的有利位置开始。
另一方面,报告中的Haskell语义仅使用denotational semantics指定。而且,在我看来,Haskell具有精确的指称语义这一事实是该语言最美丽和最有用的特性之一,它允许我一个精确的框架来思考抽象,从而精确地管理复杂性。虽然通常的抽象IO
monad没有可接受的指称语义(to the lament of some of us),但至少可以想象我们可以为它创建一个指称模型,从而保留了Haskell的一些好处。指称模型。但是,我们刚刚给出的I / O形式与Haskell的指称语义完全不兼容。
简单地说,只有两个类型为()
的可区分值(模数致命错误消息):()
和⊥。如果我们将FFI视为I / O的基础并仅使用IO
monad"作为约定",那么我们有效地为每个类型添加数千个值 - 为了继续使用指称语义,每个值必须与在评估之前执行I / O的可能性相邻,并且由于引入的额外复杂性,我们基本上失去了考虑任何两个不同的程序等效的能力,除了最多琐碎的案例 - 也就是说,我们失去了重构的能力。
当然,由于unsafePerformIO
这在技术上已经是这样,高级Haskell程序员也需要考虑操作语义。但大多数时候,包括使用I / O时,我们都可以忘记所有这些并自信地重构,正是因为我们已经了解到当我们使用unsafePerformIO
时,我们必须非常小心确保它能很好地发挥作用,它仍然为我们提供尽可能多的指称推理。如果函数有unsafePerformIO
,我会自动给它比常规函数多5或10倍,因为我需要了解有效的使用模式(通常类型签名告诉我需要知道的一切),我需要考虑缓存和竞争条件,我需要考虑我需要多深才能强制其结果等等。这太可怕了[1]。 FFI I / O需要同样的谨慎。
总结:是的,它是一个惯例,但如果你不遵循它,那么我们就不会有好事。
[1]实际上,我认为它非常有趣,但是始终考虑所有这些复杂性肯定是不切实际的。
答案 3 :(得分:4)
这取决于“是”的含义是什么 - 或者至少是“约定”的含义。
如果“惯例”的意思是“通常做事的方式”或“关于某一事项的当事人之间的协议”那么很容易给出一个无聊的答案:是的,IO
monad是一个惯例。这是该语言的设计者同意处理IO操作的方式以及该语言的用户通常执行IO操作的方式。
如果我们被允许选择一个更有趣的“约定”定义,那么我们可以得到一个更有趣的答案。如果“惯例”是其用户为了在没有语言本身帮助的情况下实现特定目标而对语言强加的规则,则答案为否:IO
monad是相反大会的强烈。它是一种由语言强制执行的学科,它协助用户构建和推理程序。
IO
类型的目的是在“纯”值的类型和需要运行时系统执行以生成有意义结果的值类型之间创建明确的区别。 Haskell类型系统强制执行这种严格的分离,防止用户(例如)创建类型Int
的值来启动众所周知的导弹。这不是第二种意义上的惯例:它的整个目标是以安全和一致的方式移动执行副作用所需的规则从用户和到语言及其编译器。
你是否可以将FFI转换为libc.so而不是执行IO并跳过IO Monad的事情?
当然,可以在没有IO monad的情况下执行IO:几乎可以看到所有其他现存的编程语言。
无论如何还是结果还是结果不确定因为Haskell评估懒惰或其他东西,比如GHC是IO Monad的模式匹配,然后以特殊方式或其他方式处理它。
没有免费午餐这样的东西。如果Haskell允许任何值要求执行涉及IO,那么它将不得不丢失我们重视的其他东西。其中最重要的可能是引用透明度:如果myInt
有时可能1
,有时候5
取决于外部因素,那么我们将失去大部分能够以严格的方式推理我们的程序(称为等式推理)。
在其他答案中提到了懒惰,但懒惰的问题特别是分享将不再安全。如果x
中的let x = someExpensiveComputationOf y in x * x
不是引用透明的,那么GHC将无法共享工作,并且必须计算两次。
真正的原因是什么?
如果没有将有效值与IO
提供的非有效值严格分开并由编译器强制执行,Haskell将不再有效地停止为Haskell。有很多语言没有强制执行这门学科。至少有一个这样做会很好。
最后,你最终会以副作用结束。那么为什么不以简单的方式去做呢?
是的,最后您的程序由一个名为main
且具有IO类型的值表示。但问题不在于你最终的位置,而是开始:如果你开始能够以严谨的方式区分有效和无效的价值观,那么你就会获得很多优势。构建该计划。
答案 4 :(得分:0)
真正的原因是什么 - 最终你最终使用了副作用,那么为什么不以简单的方式来做呢?
...你的意思是像标准机器学习?嗯,这是要付出代价的——而不是being able to write:
any :: (a -> Bool) -> [a] -> Bool
any p = or . map p
你必须输入:
any :: (a -> Bool) -> [a] -> Bool
any p [] = False
any p (y:ys) = y || any p ys
<块引用>
你能不能不只是 FFI 进入 libc.so 而是做你的 I/O,并跳过整个 IO-monad 的事情?
让我们重新表述这个问题:
<块引用>你能不能像标准机器学习那样只做 I/O,而跳过整个 IO-monad 的事情?
...因为这实际上是您想要做的。为什么要“尝试”?
SML 严格,依赖于语法顺序来指定无处不在的评估顺序 ;
Haskell 是非严格,并且依赖于数据依赖来指定某些表达式的求值顺序,例如I/O 操作。
所以:
<块引用>它无论如何都会起作用,或者结果是不确定的,因为:
(a) Haskell 的惰性求值?
(a) - 非严格语义和可见效果的组合通常没用。如需了解这种组合有多么无用的有趣展览,请观看 Erik Meiyer 的 this presentation(幻灯片可在 here 中找到)。