Haskell的计算顺序特性
首先需要知道,Haskell使用first class function1,而且使用curried function.
- first class就是指这种元素可以传给子程序作为参数,可以从子程序里返回,可以赋给变量。Haskell里面的函数什么都能做,与数值无异。我们在下文中不区分“值”的概念,“值”既可以表示一个具体的数,布尔,也可以表示一个具体的函数。
- curried就是指函数只有一个参数,而返回其余的部分(一个数值或一个低阶函数)作为返回结果。例如,函数类型为
(a -> b) -> c -> (d -> d)
,则其接收一个参数a -> b
,而返回一个类型为c -> (d -> d)
为结果。
然后需要知道Haskell中表达式的书写格式和求值方式,这也是大部分FP语言中使用的格式。熟悉这种格式才能读懂Haskell表达式,知道式子对应怎样的求值树。
-
Haskell中的默认计算顺序是左结合的。 例如,
f g h x
的计算顺序是((f g) h) x
,函数f接收g为参数。再例如,sin cos 1
的计算顺序是(sin cos) 1
,其中sin接受cos为参数。我们发现,这个式子在语法上是错误的,正确的计算方式是sin (cos 1)
。 -
Haskell中定义函数的方式是定义模式匹配的规则。 在求值时,对于第一个token对应的函数,Haskell按照这个函数定义的若干种产生规则,将token序列的头部和这些产生规则依次(从前到后)进行匹配。一旦序列的左侧头部成功匹配了某个产生式的左部,Haskell会把对应的头部序列换为产生式右部的模式,然后继续执行上述的步骤。
-
Haskell中的优先级
- 优先应用函数,然后应用运算符。运算符级别高的先执行。
- 在以中缀的方式使用函数时,其优先级为运算符的最高优先级,且默认左结合。也可以使用形如
infixl 7 `op`
的方式定义中缀的优先级和结合性。
Haskell的求值过程
在不考虑运算符的情况下,我们把Haskell的求值过程用一个输入队列和一个状态栈描述:
- 系统从输入队列读到一个值,若栈为空,则留在已解析的栈内。比如这个值的类型为
(a -> b) -> c -> (d -> d)
,则期待下一个输入的词法记号对应一个(a->b)
的值,从而把栈中的状态apply到这个值,留下函数应用结果c -> (d -> d)
类型的值在栈中。 - 若栈中已有一个值,则将从输入队列读到的值作为参数,传入栈内已有的值(函数)中,进行apply,得到函数值(数或低阶函数)。
- 如果读到一个中缀函数,则期待栈内已经有一个满足其类型限定的值;中缀函数以栈中值作为参数传入,完成apply,结果保存在栈中。
这也就是为什么sin cos 1
是错的。在读入sin
时,系统期待下一个读入的值是一个Num a => a
类型的数值,但读入的cos
是Num a => a -> a
类型的,无法匹配,从而报错。
在考虑运算符的情况下,我们认为运算符把式子分割成多个不相关的部分,如括号一样。我们先对各个部分进行求值,然后按照运算符的优先级和结合律依次进行处理。type inference,如果有,也是相同的道理。
参考
first class https://www.jianshu.com/p/c054c59c5fda ↩︎