采取以下迷你语言:
data Action = Get (Char -> Action) | Put Char Action | End
Get f
意思是:读一个字符c
,执行动作f c
。
Put c a
意思是:写字c
,执行动作a
。
这是一个打印“xy”的程序,然后要求输入两个字母并以相反的顺序打印它们:
Put 'x' (Put 'y' (Get (\a -> Get (\b -> Put b (Put a End)))))
你可以操纵这样的程序。例如:
conditionally p = Get (\a -> if a == 'Y' then p else End)
这是有类型Action -> Action
的 - 它需要一个程序并提供另一个首先要求确认的程序。这是另一个:
printString = foldr Put End
这有类型String -> Action
- 它接受一个字符串并返回一个写入字符串的程序,比如
Put 'h' (Put 'e' (Put 'l' (Put 'l' (Put 'o' End))))
.
Haskell 中的 IO 工作方式类似。尽管执行它需要执行副作用,但您可以以纯粹的方式构建复杂的程序而无需执行它们。您正在计算程序的描述(IO 操作),而不是实际执行它们。
在像 C 这样的语言中,您可以编写一个void execute(Action a)
实际执行程序的函数。在 Haskell 中,您可以通过编写main = a
. 编译器创建一个执行动作的程序,但你没有其他方法来执行动作(除了肮脏的技巧)。
显然Get
,Put
不仅仅是选项,您还可以向 IO 数据类型添加许多其他 API 调用,例如对文件进行操作或并发操作。
添加结果值
现在考虑以下数据类型。
data IO a = Get (Char -> Action) | Put Char Action | End a
之前的Action
类型等价于IO ()
,即一个总是返回“unit”的IO值,相当于“void”。
这种类型与 Haskell IO 非常相似,只是在 Haskell IO 中是一种抽象数据类型(你无权访问定义,只能访问某些方法)。
这些是可以以某些结果结束的 IO 操作。像这样的值:
Get (\x -> if x == 'A' then Put 'B' (End 3) else End 4)
有类型IO Int
并且对应一个C程序:
int f() {
char x;
scanf("%c", &x);
if (x == 'A') {
printf("B");
return 3;
} else return 4;
}
评估和执行
评估和执行之间是有区别的。您可以评估任何 Haskell 表达式,并获得一个值;例如,将 2+2 :: Int 计算为 4 :: Int。您只能执行类型为 IO a 的 Haskell 表达式。这可能会产生副作用;执行Put 'a' (End 3)
将字母 a 放到屏幕上。如果您评估一个 IO 值,如下所示:
if 2+2 == 4 then Put 'A' (End 0) else Put 'B' (End 2)
你得到:
Put 'A' (End 0)
但是没有副作用 - 您只进行了评估,这是无害的。
你会怎么翻译
bool comp(char x) {
char y;
scanf("%c", &y);
if (x > y) { //Character comparison
printf(">");
return true;
} else {
printf("<");
return false;
}
}
转化为 IO 值?
修正一些字符,说'v'。现在comp('v')
是一个 IO 操作,它将给定字符与“v”进行比较。同样,comp('b')
是一个 IO 操作,它将给定字符与“b”进行比较。一般来说,comp
是一个接受一个字符并返回一个 IO 动作的函数。
作为 C 语言的程序员,您可能会争辩说这comp('b')
是一个布尔值。在 C 中,求值和执行是相同的(即它们表示相同的事情,或者同时发生)。不在哈斯克尔。comp('b')
计算为一些 IO 操作,执行后给出一个布尔值。(准确地说,它计算为上面的代码块,只是用'b'代替了x。)
comp :: Char -> IO Bool
comp x = Get (\y -> if x > y then Put '>' (End True) else Put '<' (End False))
现在,comp 'b'
评估为Get (\y -> if 'b' > y then Put '>' (End True) else Put '<' (End False))
.
这在数学上也很有意义。在 C 中,int f()
是一个函数。对于数学家来说,这没有任何意义——一个没有参数的函数?函数的重点是接受参数。一个函数int f()
应该等价于int f
. 不是,因为 C 中的函数混合了数学函数和 IO 动作。
头等舱
这些 IO 值是一流的。就像您可以拥有整数元组列表一样,[[(0,2),(8,3)],[(2,8)]]
您可以使用 IO 构建复杂值。
(Get (\x -> Put (toUpper x) (End 0)), Get (\x -> Put (toLower x) (End 0)))
:: (IO Int, IO Int)
IO 操作的元组:首先读取一个字符并将其打印为大写,然后读取一个字符并将其返回为小写。
Get (\x -> End (Put x (End 0))) :: IO (IO Int)
一个 IO 值,它读取一个字符x
并结束,返回一个写入x
屏幕的 IO 值。
Haskell 具有允许轻松操作 IO 值的特殊功能。例如:
sequence :: [IO a] -> IO [a]
它接受一个 IO 动作列表,并返回一个按顺序执行它们的 IO 动作。
单子
Monads 是一些组合子(conditionally
如上),它们允许您编写更具结构性的程序。有一个由 type 组成的函数
IO a -> (a -> IO b) -> IO b
给定 IO a 和一个函数 a -> IO b,返回一个 IO b 类型的值。如果您将第一个参数编写为 C 函数a f()
,将第二个参数编写为b g(a x)
它返回的程序g(f(x))
。鉴于上述 Action / IO 的定义,您可以自己编写该函数。
注意 monads 对纯度来说不是必需的——你总是可以像我上面那样编写程序。
纯度
纯度的本质是参考透明度,以及区分评估和执行。
在 Haskell 中,如果有f x+f x
,可以将其替换为2*f x
. 在 C中,f(x)+f(x)
一般不一样2*f(x)
,因为f
可以在屏幕上打印一些东西,或者修改x
.
由于纯度,编译器有更多的自由并且可以更好地优化。它可以重新安排计算,而在 C 语言中,它必须考虑这是否会改变程序的含义。