IO 是一种类型* -> *,也就是说它接受一个类型参数。通常,IO 表示可以执行 I/O 并产生结果的一元操作。给IO 的类型参数决定了结果的类型。因此,
IO () 是一个可以执行 I/O 并产生 () 的单子操作。 () 只有一个值,因此它不传达任何信息。由于它不传达任何信息,因此它的使用方式通常与使用 void 作为传统过程编程语言中的返回值相同。
IO r 是一个可以执行 I/O 并产生 r 的单子操作。您可能会注意到与上述语句的相似之处。区别在于r 不是像() 这样的具体类型,而是一个类型变量。
让我详细说明这意味着什么以及由此产生的后果。看id的类型:
ghci> :t id
id :: a -> a
当然,这意味着如果给id 提供a 类型的参数,它将返回相同类型a 的结果。现在检查const ()的类型:
ghci> :t const ()
const () :: a -> ()
如果我们给它一个a,它将返回一个()类型的结果。现在检查error:
ghci> :t error
error :: String -> a
我们必须给它一个String,但它的返回值可以适应我们需要的任何值。当然,由于我们不一定能构造任何给定类型的值,这意味着唯一可能的定义是从不返回一个值,这就是error 所做的。
因此,有了这种理解,您应该意识到虽然 IO r 始终表示“可以执行 I/O 并返回 r 类型的值的单子操作”,但其含义可能会因出现位置而异在类型签名中。让我们看看你的具体例子:
ghci> :t withFile
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
如果我们有一个函数返回 IO r 而没有出现在其他任何地方的 r,我们可以得出的唯一结论是 IO 绝不能产生值,否则我们将无法声称它可以返回任意的r。幸运的是,情况并非如此:确实出现了另一个r。 withFile 接受一个返回 IO r 的函数。因为withFile 产生一个IO,它创建一个r,它知道如何创建一个r 的唯一方法是通过我们给它的函数,我们知道如果它要终止,它必须执行我们给它的功能至少一次。此外,我们知道它必须返回它从中获得的 rs 之一。
所以,在withFile 的上下文中,IO r 意味着如果你给它一个函数来产生一个返回某些特定类型的一元动作,withFile 最终会给你一个产生该类型的一元动作, 也。举个具体的例子:
myInt <- withFile "number.txt" ReadMode (fmap read . hGetContents)
print (myInt + (1 :: Int))
hGetContents,给定一个Handle,将返回一个IO String。 fmap read 将把它变成IO Int。由于withFile 是使用类型变量定义的,没有指定任何特定的具体类型,因此它将适应我们给它的类型(Handle -> IO Int)并返回IO Int。然后我们可以在do 符号块中使用<- 来执行它并将myInt 绑定到结果。返回值通过withFile冒泡。