今天的《计算概论(A):函数式程序设计》课上讲到了运算符和运算符部分应用(operator sectioning)。这里将详细总结了Haskell中和运算符有关的大部分内容,作为补充和备忘。
本文的主要参考Haskell 2010 Report(1)中的BNF文法。
运算符的词法规则
预备知识:标识符
Haskell的标识符(identifier)分入两个命名空间:变量标识符(varid)和构造器标识符(conid);这二者以首字母的大小写相区别:以大写字母开头的是构造器标识符,以小写字母或下划线(_
)开头的是变量标识符。
值得注意的是,Haskell的输入字符集是Unicode,因此大写字母包括了uppercase和titlecase的Unicode字母,小写字母包括了lowercase的Unicode字母。一些相关信息请参阅Unicode FAQ - Character Properties, Case Mappings & Names。
限定的标识符(qualified identifier)是指带模块名限定的标识符,例如在模块M
中的函数f
可以表示为M.f
。限定的标识符也视为一个完整的词法记号。
预备知识:符号
符号是由若干个ASCII符号字符和Unicode符号字符构成的。ASCII符号字符包括!
、#
、$
、%
、&
、*
、+
、.
、/
、<
、=
、>
、?
、@
、\
、ˆ
、-
、~
和:
。Unicode符号字符包括所有的符号(symbol)和标点符号(punctuation)。一些Unicode符号的相关信息请参阅Unicode FAQ - Punctuation and Symbols。
和标识符一样,符号也被分入两个命名空间:变量符号(varsym)和构造器符号(consym);这二者以符号的第一个字符相区别:以冒号(:
)开头的符号是构造器符号,其余符号都是变量符号。
运算符
运算符可按其所在的命名空间划分为变量运算符(varop)和构造器运算符(conop),但通常被区分为符号构成的普通运算符(如&&
、:
等)以及标识符转换成的运算符(如`div`
、`isPrefixOf`
等)。
普通运算符
为简便起见,我们这里不考虑Unicode字符。那么,用若干个!#$%&*+./<=>?@\ˆ-~:
字符即可构成一个普通运算符。和C/C++、Rust等语言不同,Haskell中所有的运算符一视同仁,可以任意定义,除了少数语言保留的运算符以外,大多数常见的运算符都是由标准库Prelude
定义的,并不涉及语言的黑魔法。
语言保留的运算符如下:..
、:
、::
、=
、\
、|
、<-
、->
、@
、~
、=>
。这些都参与构成了语言的最基本语法,不能被重新用于其他目的。(:
运算符被列在这里应该是因为Haskell语言中列表的语法糖:[a, b, c, d]
转换成a : b : c : d : []
。)
Haskell中几乎所有运算符都是二元中缀运算符(binary infix operator)。唯一的例外是“-
”,它同时是减号和负号,因此可以用作一元前缀运算符(unary prefix operator)。
标识符转换成运算符
在标识符的两边加上反引号(`
),就得到一个二元中缀运算符。例如,函数div :: Fractional a => a -> a -> a
,可以转换成运算符`div`
。
反引号一般位于键盘上Tab键的上方,数字1的左侧。
注意这里只能使用标识符。也就是说,表达式不可以这样转换为运算符。例如,定义multDiv x y z = x * y / z
,不可以把multDiv a b c
写成b `multDiv a` c
,但是可以写成(a `multDiv` b) c
。
运算符的结合性和优先级
Haskell中,除减号外都是中缀运算符,因此需要考虑结合性(fixity)和优先级(precedence)。运算符有三种结合性(左结合infixl
、右结合infixr
、不结合infix
),10种优先级(9~0,数值越大表明运算符优先级越高)。表达式先按照优先级、同优先级再按照结合性决定结合顺序,如果表达式结合顺序未决,则编译出错。
结合顺序未决发生在同优先级的运算符不结合时。例如:对表达式True == True == False
而言,由于==
不结合,编译出错:“Precedence parsing error: cannot mix ‘==’ [infix 4] and ‘==’ [infix 4] in the same infix expression
”(解析优先级出错:在同一个中缀表达式中,不能混用优先级为4、不结合的运算符==
)。
所有运算符都可以指定优先级和结合性,包括标识符转换成的运算符。例如,infixl 7 `div`
为运算符`div`
指定为“左结合、优先级为7”。
语言定义的标准运算符优先级和结合性参看下表:
优先级 | 左结合 | 不结合 | 右结合 |
---|---|---|---|
9 | !! |
. |
|
8 | ^ ^^ ** |
||
7 | * / `div` `mod` `rem` `quot` |
||
6 | + - |
||
5 | : ++ |
||
4 | == /= < <= > >= `elem` `notElem` |
||
3 | && |
||
2 | || |
||
1 | >> >>= |
||
0 | $ $! `seq` |
运算符的部分应用
运算符的部分应用,即省略中缀运算符的两个参数之一,得到一元函数。两种形式分别是(op e)
和(e op)
,下面以(op e)
为例。(op e)
是合法的表达式,当且仅当x op e
和x op (e)
解析为同样的表达式。例如(*a+b)
不是合法的表达式,但(+a*b)
和(*(a+b))
都是合法的。
注意括号是语法的一部分,不能省略。
如果op
是一个二元运算符,变量x
不在表达式e
中自由出现,那么(op e)
和\x -> x op e
以及(e op)
和\x -> e op x
分别严格相同。
有一个例外需要注意,因为“-
”同时是减号和负号,语言规定(- a)
解释成“负的a
”而不是“减去a
”(\x -> x - a
)。要表示“减去a
”的含义,可以使用subtract a
,也可以用(+ (- a))
表示同义的函数。