请看下面的迷你语言:
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
不是唯一的选项,您可以将许多其他API调用添加到IO数据类型中,例如对文件或并发性进行操作。
添加结果值
现在考虑以下数据类型。
data IO a = Get (Char -> Action) | Put Char Action | End a
前面提到的Action
类型相当于IO ()
,即一个始终返回"unit"(可视为"void")的IO值。
这个类型与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) {
printf(">");
return true;
} else {
printf("<");
return false;
}
}
如何将其转换为IO值?
首先,我们需要修复一些字符,比如 'v'。现在 comp('v')
是一个IO操作,它会将给定的字符与 'v' 进行比较。同样地,comp('b')
是一个IO操作,它会将给定的字符与 'b' 进行比较。通常情况下,comp
是一个接受字符并返回IO操作的函数。
作为一个C程序员,你可能会认为 comp('b')
是一个布尔值。在C语言中,评估和执行是相同的(即它们意味着相同的事情,或者同时发生)。但在Haskell中不是这样的。 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)
一个读取字符 x
并结束的 IO 值,返回一个将 x
写入屏幕的 IO 值。
Haskell 有特殊的函数,可以轻松操作 IO 值。例如:
sequence :: [IO a] -> IO [a]
这个函数接受一个 IO 操作列表,并返回一个按顺序执行它们的 IO 操作。
单子
单子是一些组合子(比如上面的 conditionally
),它们允许您更结构化地编写程序。有一个类型为的组合函数
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的定义,您可以自己编写该函数。
请注意,单子对于纯度并不是必需的-您始终可以像我上面那样编写程序。
纯度
有关纯度的重要事情是引用透明性,并区分评估和执行。
在Haskell中,如果您有f x+f x
,则可以将其替换为2*f x
。在C中,f(x)+f(x)
通常不等同于2*f(x)
,因为f
可能会在屏幕上打印某些内容或修改x
。
由于纯度,编译器具有更大的自由度,可以更好地进行优化。它可以重新排列计算,而在C中,它必须考虑是否改变了程序的含义。