问题不是做什么IO
,而是它是如何定义的,它的签名。具体来说,这个是数据还是类,a
那么“”是它的类型参数吗?我在任何地方都没有找到它。另外,我不明白这个的语法含义:
f :: IO a
问题不是做什么IO
,而是它是如何定义的,它的签名。具体来说,这个是数据还是类,a
那么“”是它的类型参数吗?我在任何地方都没有找到它。另外,我不明白这个的语法含义:
f :: IO a
您问是否IO a
是数据类型:它是。你问是否a
是它的类型参数:它是。你说你找不到它的定义。让我告诉你如何找到它:
localhost:~ gareth.rowlands$ ghci
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Prelude> :i IO
newtype IO a
= GHC.Types.IO (GHC.Prim.State# GHC.Prim.RealWorld
-> (# GHC.Prim.State# GHC.Prim.RealWorld, a #))
-- Defined in `GHC.Types'
instance Monad IO -- Defined in `GHC.Base'
instance Functor IO -- Defined in `GHC.Base'
Prelude>
在 ghci 中,:i
或者:info
告诉你一个类型。它显示类型声明及其定义位置。你可以看到那IO
是aMonad
和a Functor
。
这种技术对普通的 Haskell 类型更有用——正如其他人所指出的,IO
这在 Haskell 中很神奇。在典型的 Haskell 类型中,类型签名非常具有启发性,但重要的IO
不是它的类型声明,而是IO
实际执行的操作IO
。他们以非常传统的方式执行此操作,通常通过调用底层 C 或 OS 例程。例如,Haskell 的putChar
动作可能会调用 C 的putchar
函数。
IO
是一个多态类型(恰好是 的一个实例Monad
,在这里无关)。
考虑一下不起眼的清单。如果我们要编写自己的Int
s 列表,我们可以这样做:
data IntList = Nil | Cons { listHead :: Int, listRest :: IntList }
如果你然后抽象它是什么元素类型,你会得到:
data List a = Nil | Cons { listHead :: a, listRest :: List a }
如您所见, 的返回listRest
值为List a
。List
是 kind 的多态类型,* -> *
也就是说它需要一个类型参数来创建具体类型。
以类似的方式,IO
是一个带有 kind 的多态类型* -> *
,这再次意味着它接受一个类型参数。如果您要自己定义它,它可能如下所示:
data IO a = IO (RealWorld -> (a, RealWorld))
(定义由这个答案提供)
魔法的数量IO
被严重高估了:它得到了编译器和运行时系统的一些支持,但比新手通常期望的要少得多。
这是定义它的源文件:
http://www.haskell.org/ghc/docs/latest/html/libraries/ghc-prim-0.3.0.0/src/GHC-Types.html
newtype IO a
= IO (State# RealWorld -> (# State# RealWorld, a #))
它只是 state monad 的优化版本。如果我们删除优化注释,我们将看到:
data IO a = IO (Realworld -> (Realworld, a))
所以基本上IO a
是一个存储一个函数的数据结构,该函数采用旧的现实世界并返回新的现实世界并执行 io 操作a
。
一些编译器技巧主要是为了Realworld
有效地删除虚拟值。
IO 类型是抽象的newtype
- 构造函数不会被导出,所以你不能绕过库函数,直接使用它并执行令人讨厌的事情:重复RealWorld
,RealWorld
无中生有或逃避 monad(编写IO a -> a
类型的函数)。
由于 IO 可以应用于任何类型 a 的对象,因为它是一个多态 monad,所以没有指定 a。
如果您有一些类型为 a 的对象,那么它可以被“包装”为类型 IO a 的对象,您可以将其视为提供类型 a 的对象的操作。例如,getChar 是 IO Char 类型,因此当它被调用时,它具有(从程序的角度)生成一个来自 stdin 的字符的副作用。
再举一个例子,putChar 的类型是 Char -> IO (),这意味着它接受一个 char,然后执行一些不提供输出的操作(在程序的上下文中,尽管它会将给定的 char 打印到 stdout)。
编辑:单子的更多解释:
monad 可以被认为是一个“包装类型”M,并且有两个相关的函数:
return 和 >>=。
给定类型 a,可以使用 return 函数创建类型为 M a 的对象(在 IO monad 的情况下为 IO a)。
因此,return 的类型为 a -> M a。此外,return 试图不改变它传递的元素——如果你调用 return x,你将得到一个包含 x 的所有信息的 x 的包装版本(至少理论上,这不会发生,例如,空单子。)
例如,返回“x”将产生一个 M 字符。这就是 getChar 的工作原理——它使用 return 语句生成一个 IO Char,然后用 <- 将其从包装器中拉出。
>>=,读作'bind',更复杂。它的类型是 M a -> (a -> M b) -> M b,它的作用是获取一个“包装”对象,以及从该对象的底层类型到另一个“包装”对象的函数,并应用该函数到第一个输入中的基础变量。
例如, (return 5) >>= (return . (+ 3)) 将产生一个 M Int,这将与 return 8 给出的 M Int 相同。这样,任何可以在外部应用的函数也可以在其中应用 monad。
为此,可以采用任意函数 f :: a -> b,并给出新函数 g :: M a -> M b,如下所示:
g x = x >>= (return . f)
现在,要成为 monad,这些操作也必须有一定的关系——它们上面的定义还不够。
首先: (return x) >>= f 必须等价于 f x。也就是说,无论 x 是否“包装”在 monad 中,它都必须等效于对 x 执行操作。
第二:x >>= return 必须等价于 m。也就是说,如果一个对象被 bind 解包,然后通过 return 重新包装,它必须返回到相同的状态,保持不变。
第三,最后 (x >>= f) >>= g 必须等价于 x >>= (\y -> (fy >>= g) )。也就是说,函数绑定是关联的(有点)。更准确地说,如果两个函数被先后绑定,这必然相当于绑定了它们的组合。
现在,虽然这是 monad 的工作方式,但它不是最常用的方式,因为 do 和 <- 的语法糖。
本质上,do 开始了一个长长的绑定链,每个 <- 都会创建一个被绑定的 lambda 函数。
例如,
a = do x <- something
y <- function x
return y
相当于
a = something >>= (\x -> (function x) >>= (\y -> return y))
在这两种情况下,某些东西都绑定到 x,函数 x 绑定到 y,然后 y 在相关 monad 的包装器中返回给 a。
对不起,文字墙,我希望它能解释一些东西。如果您需要对此进行澄清,或者此解释中的某些内容令人困惑,请询问。
如果你问我,这是一个非常好的问题。我记得我也对此感到非常困惑,也许这会有所帮助......
'IO'是一个类型构造函数,'IO a'是一个类型,'a'(在'IO a'中)是一个类型变量。字母“a”没有意义,字母“b”或“t1”也可以使用。
如果查看 IO 类型构造函数的定义,您会发现它是一个新类型,定义为:GHC.Types.IO (GHC.Prim.State# GHC.Prim.RealWorld -> (# GHC.Prim.State# GHC .Prim.RealWorld, a #))
'f :: IO a' 是一个名为 'f' 的函数的类型,显然没有参数,它返回 IO monad 中某些不受约束的类型的结果。'in the IO monad' 意味着 f 可以在计算结果时执行一些 IO(即更改 'RealWorld',其中 'change' 意味着用新的 RealWorld 替换提供的 RealWorld)。f 的结果是多态的(这是一个类型变量 'a' 而不是像 'Int' 这样的类型常量)。多态结果意味着在您的程序中,调用者决定了结果的类型,因此在一个地方使用 f 可以返回一个 Int,在另一个地方使用它可以返回一个 String。“不受约束”意味着没有类型类限制可以返回的类型,因此可以返回任何类型。
为什么'f'是一个函数而不是一个常数,因为没有参数并且Haskell是纯的?因为 IO 的定义意味着 'f :: IO a' 可以写成 'f :: GHC.Prim.State# GHC.Prim.RealWorld -> (# GHC.Prim.State# GHC.Prim.RealWorld, a #)' 所以实际上有一个参数——“现实世界的状态”。
数据IO a
a
中的含义主要与 中相同Maybe a
。但是我们不能摆脱一个构造函数,比如:
fromIO :: IO a -> a
fromIO (IO a) = a
幸运的是,我们可以在 Monads 中使用这些数据,例如:
{-# LANGUAGE ScopedTypeVariables #-}
foo = do
(fromIO :: a) <- (dataIO :: IO a)
...