Haskell中的monadic IO构造只是一个约定,还是有一个实现原因呢?
你能不能只用FFI进入libc.so而不是你的IO,并跳过IO Monad组件?
无论如何它会起作用,或者结果是不确定的,因为Haskell评估懒惰或其他东西,比如GHC是IO Monad的模式匹配,然后以特殊方式或其他方式处理它.
真正的原因是什么?最后你最终会产生副作用.那么为什么不这么简单呢?
Ale*_*ing 69
是的,monadic I/O是Haskell懒惰的结果.但具体来说,monadic I/O是Haskell 纯粹的结果,这对于懒惰语言是可预测的实际上是必要的.†
这很容易通过一个例子来说明.想象一下哈斯克尔不是纯粹的,但它仍然是懒惰的.它不是putStrLn具有类型String -> IO (),而是简单地具有类型String -> (),并且它将字符串作为副作用打印到stdout.这样做的麻烦在于,这只会在putStrLn实际调用时发生,而在惰性语言中,仅在需要结果时调用函数.
这是麻烦:putStrLn生产().看一下类型的值()是没用的,因为()意思是"无聊".这意味着该程序可以达到您的预期:
main :: ()
main =
case putStr "Hello, " of
() -> putStrLn " world!"
-- prints “Hello, world!\n”
Run Code Online (Sandbox Code Playgroud)
但我认为你可以同意编程风格很奇怪.case ... of但是,这是必要的,因为它强制putStr通过匹配来评估呼叫().如果你稍微调整程序:
main :: ()
main =
case putStr "Hello, " of
_ -> putStrLn " world!"
Run Code Online (Sandbox Code Playgroud)
...现在它只打印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` ()
Run Code Online (Sandbox Code Playgroud)
这个程序打印出来first\nsecond\n还是second\nfirst\n?在不知道(+)评估其论点的顺序的情况下,我们不知道.在Haskell中,评估顺序甚至不总是定义明确,因此完全有可能实现两个效果的顺序实际上完全无法确定!
在具有明确定义的评估顺序的严格语言中不会出现此问题,但在像Haskell这样的惰性语言中,我们需要一些额外的结构来确保副作用是(a)实际评估和(b)以正确的顺序执行.Monads碰巧是一个优雅地提供必要结构来强制执行该命令的接口.
这是为什么?那怎么可能呢?好吧,monadic接口在签名中提供了数据依赖的概念>>=,它强制执行明确定义的评估顺序.Haskell的实现IO是"魔术",从某种意义上说它是在运行时实现的,但是monadic接口的选择远非任意.在纯语言中编码顺序动作的概念似乎是一种相当好的方法,它使得Haskell可以在不牺牲可预测的效果排序的情况下保持懒惰和引用透明.
值得注意的是,monad不是以纯粹方式编码副作用的唯一方法 - 事实上,从历史上看,它们甚至不是Haskell处理副作用的唯一方式.不要误以为monad只用于I/O(它们不是),只在懒惰的语言中有用(即使用严格的语言它们对于保持纯度也很有用),只能用于纯语言(很多东西都是有用的monad,不仅仅是为了强制执行纯度),或者你需要monad来做I/O(你没有).不过,他们确实在Haskell中为这些目的做得很好.
†关于这一点,西蒙佩顿琼斯曾经指出,"懒惰使你诚实"纯洁.
Mic*_*mza 24
您是否可以将FFI转换为libc.so而不是执行IO并跳过IO Monad的事情?
取自https://en.wikibooks.org/wiki/Haskell/FFI#Impure_C_Functions,如果将FFI函数声明为纯(因此,不引用IO),则
GHC认为计算两次纯函数结果没有意义
这意味着函数调用的结果被有效地缓存.例如,声明外部不纯伪随机数生成器返回a的程序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)
Run Code Online (Sandbox Code Playgroud)
每次调用都返回相同的东西,至少在我的编译器/系统上:
16807
16807
Run Code Online (Sandbox Code Playgroud)
如果我们更改声明以返回a 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
Run Code Online (Sandbox Code Playgroud)
然后这导致(可能)每个调用返回一个不同的数字,因为编译器知道它是不纯的:
16807
282475249
Run Code Online (Sandbox Code Playgroud)
因此,无论如何,您都必须使用IO来调用标准库.
luq*_*qui 12
假设使用FFI我们定义了一个函数
c_write :: String -> ()
Run Code Online (Sandbox Code Playgroud)
这取决于它的纯度,因为每当它的结果被强制它打印字符串.因此,我们不会遇到Michal的答案中的缓存问题,我们可以定义这些函数以进行额外的()论证.
c_write :: String -> () -> ()
c_rand :: () -> CUInt
Run Code Online (Sandbox Code Playgroud)
在实现级别上,只要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
Run Code Online (Sandbox Code Playgroud)
基本上,我们只是将所有Alexis笨拙的使用问题都填充到monad中,只要我们使用monadic接口,一切都保持可预测.从这个意义上说,IO这只是一个约定 - 因为我们可以在Haskell中实现它,没有任何基本的东西.
这是从运营的有利位置.
另一方面,Haskell在报告中的语义仅使用指称语义来指定.而且,在我看来,Haskell具有精确的指称语义这一事实是该语言最美丽和最有用的特性之一,它允许我一个精确的框架来思考抽象,从而精确地管理复杂性.虽然通常的抽象IOmonad没有公认的指称语义(对于我们中的一些人来说),但至少可以想象我们可以为它创建一个指称模型,从而保留了Haskell指称模型的一些好处.但是,我们刚刚给出的I/O形式与Haskell的指称语义完全不兼容.
简单地说,只有两个可区分的值(模数致命错误消息)类型(): ()和⊥.如果我们将FFI视为I/O的基础并且IO仅使用monad"作为约定",那么我们有效地为每种类型添加数十亿个值- 继续具有指称语义,每个值必须与执行的可能性相邻在评估之前的I/O,以及这引入的额外复杂性,我们基本上失去了考虑任何两个不同程序等效的能力,除非在最琐碎的情况下 - 也就是说,我们失去了重构的能力.
当然,因为unsafePerformIO技术上已经是这种情况,高级Haskell程序员也需要考虑操作语义.但大多数时候,包括使用I/O时,我们都可以忘记所有这些并自信地重构,正是因为我们已经了解到,当我们使用时unsafePerformIO,我们必须非常小心地确保它能够很好地运行,它仍然可以提供我们尽可能多地进行指称推理.如果一个函数有unsafePerformIO,我自动给它比普通函数多5或10倍,因为我需要了解有效的使用模式(通常类型签名告诉我需要知道的一切),我需要考虑缓存和竞争条件,我需要考虑我需要多深才能强制其结果等等.这太糟糕了[1].FFI I/O需要同样的谨慎.
总结:是的,这是一个惯例,但如果你不遵循它,那么我们就不能拥有美好的东西.
[1]实际上我认为这很有趣,但是一直考虑所有这些复杂性肯定是不切实际的.
| 归档时间: |
|
| 查看次数: |
3397 次 |
| 最近记录: |