Haskell运算符全解

 

今天的《计算概论(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 ex 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))表示同义的函数。

参考文献

  1. Haskell 2010 Report.