IO 的角色和目的
Haskell 是一种纯语言,意味着表达式不会产生副作用。副作用是表达式或函数除了产生值之外的任何其他作用,例如,修改全局计数器或打印到标准输出。
在 Haskell 中,使用 IO
对有效计算(特别是可以对现实世界产生影响的计算)进行建模。严格来说,IO
是一个类型构造函数,采用类型并生成类型。例如,IO Int
是产生 Int
值的 I / O 计算的类型。IO
类型是抽象的,并且为 IO
提供的接口通过确保执行 IO 的所有内置函数都具有包含在 IO
中的返回类型来确保某些非法值(即,具有非敏感类型的函数)不存在。
当运行 Haskell 程序时,执行由名为 main
的 Haskell 值表示的计算,其类型可以是任何类型 x
的 IO x
。
操作 IO 值
标准库中有许多函数提供通用编程语言应执行的典型 IO
操作,例如读取和写入文件句柄。一般 IO
动作创建并主要与两个功能组合:
(>>=) :: IO a -> (a -> IO b) -> IO b
这个函数(通常称为 bind )接受一个 IO
动作和一个返回 IO
动作的函数,并产生 IO
动作,这是将函数应用于第一个 IO
动作产生的值的结果。
return::a -> IO a
此函数接受任何值(即纯值)并返回不执行 IO 并生成给定值的 IO 计算。换句话说,它是一个无操作的 I / O 操作。
还有一些常用的常用功能,但所有功能都可以根据上述两种方式编写。例如,(>>) :: IO a -> IO b -> IO b
类似于 (>>=)
,但第一个动作的结果被忽略。
一个简单的程序,使用这些函数来问候用户:
main::IO ()
main =
putStrLn "What is your name?" >>
getLine >>= \name ->
putStrLn ("Hello " ++ name ++ "!")
该程序还使用 putStrLn::String -> IO ()
和 getLine::IO String
。
注意:上面某些函数的类型实际上比给定的类型更通用(即 >>=
,>>
和 return
)。
IO 语义
Haskell 中的 IO
类型与命令式编程语言的语义非常相似。例如,当一个用命令语言写 s1 ; s2
来表示执行语句 s1
,然后声明 s2
时,可以写一个 s1 >> s2
来模拟 Haskell 中的同一个东西。
然而,IO
的语义略微偏离了强制性背景所预期的内容。return
函数不会中断控制流 - 如果按顺序运行另一个 IO
操作,它对程序没有影响。例如,return () >> putStrLn "boom"
正确地将 boom
打印到标准输出。
IO
的形式语义可以通过涉及上一节函数的简单均等来给出:
return x >>= f ≡ f x, ∀ f x
y >>= return ≡ return y, ∀ y
(m >>= f) >>= g ≡ m >>= (\x -> (f x >>= g)), ∀ m f g
这些通常分别称为左侧身份,右侧身份和组成。它们可以在函数方面更自然地陈述
(>=>) :: (a -> IO b) -> (b -> IO c) -> a -> IO c
(f >=> g) x = (f x) >>= g
如下:
return >=> f ≡ f, ∀ f
f >=> return ≡ f, ∀ f
(f >=> g) >=> h ≡ f >=> (g >=> h), ∀ f g h
懒惰的 IO
执行 I / O 计算的函数通常是严格的,这意味着必须在下一个操作开始之前完成一系列操作中的所有先前操作。通常这是有用的和预期的行为 - putStrLn "X" >> putStrLn "Y"
应该打印 XY
。但是,某些库函数会懒惰地执行 I / O,这意味着生成值所需的 I / O 操作仅在实际使用该值时执行。这些功能的例子是 getContents
和 readFile
。惰性 I / O 可以大大降低 Haskell 程序的性能,因此在使用库函数时,应注意注意哪些函数是惰性的。
IO 和 do
表示法
Haskell 提供了一种将不同 IO 值组合成更大 IO 值的更简单方法。这种特殊的语法被称为 do
符号*,它只是用于 >>=
,>>
和 return
函数的语法糖。
上一节中的程序可以使用 do
表示法以两种不同的方式编写,第一种是布局敏感,第二种是布局不敏感:
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello " ++ name ++ "!")
main = do {
putStrLn "What is your name?" ;
name <- getLine ;
putStrLn ("Hello " ++ name ++ "!")
}
这三个程序完全相同。
*请注意,do
表示法也适用于更广泛的类型构造函数 monad 。