标准 ML 编程/表达式
标准 ML 程序由一系列“词法单元”组成;它们可以被认为是语言的“词”。一些最常见的词法单元类型是
词法单元类型 | 示例 |
---|---|
特殊常量 | 2 , ~5.6 , "string" , #"c" |
字母数字标识符 | x , mod , 'a |
符号标识符 | + , - , * , /
|
关键字 | val , = , ( , ) |
算术表达式类似于许多其他语言中的算术表达式
3 + 4 3.0 / 4.0 (2 - 3) * 6
但是,需要注意一些要点
- 一元否定使用波浪号表示~而不是连字符-;后者仅用于二元减法。例如,三减负二写为3 - ~2或者3 - ~ 2.
- 虽然运算符“重载”以支持多种数值类型——例如,两者都是3 + 4和3.0 + 4.0都是有效的(前者类型为
int
,后者类型为real
)——但类型之间没有隐式提升。因此,像这样的表达式3 + 4.0是无效的;必须编写3.0 + 4.0,或者real 3 + 4.0(使用基本函数real将3转换为3.0). - 整数除法使用特殊运算符表示div而不是斜线/;后者仅用于实数除法。(由于类型之间没有隐式提升,因此像这样的表达式3 / 4是无效的;必须编写3.0 / 4.0或者real 3 / real 4。)还有一个取模运算符mod;例如,十七除以五是三余二,所以17 div 5是3和17 mod 5是2。(更一般地说,如果 q 是一个正整数,那么d = p div q和m = p mod q是整数,使得p = d * q + m, m >= 0,并且m < q。如果 q 是一个负整数,那么p = d * q + m, m <= 0,并且m > q.)
一旦函数被声明
fun triple n = 3 * n
只需在函数名后添加一个参数即可调用它
val twelve = triple 4
在一般情况下,圆括号不是必需的,但它们通常是必要的用于分组。此外,正如我们在类型章节中所看到的,元组使用圆括号构建,并且将元组作为函数参数构建并不罕见
fun diff (a, b) = a - b val six = diff (9, 3)
函数调用具有非常高的优先级,高于任何中缀运算符;因此,triple 4 div 5
表示 (3 * 4) div 5
,结果为 2,而不是 3 * (4 div 5)
,结果为 0。此外,它们是左结合的;f x y
表示 (f x) y
(其中 f
以 x
作为参数并返回一个接受 y
作为其参数的函数),而不是 f (x y)
。
二元函数——即参数类型为 2 元组类型的函数——可以转换为中缀运算符
fun minus (a, b) = a - b val three = minus (5, 2) infix minus val seven = 4 minus ~3 val two = op minus (20, 18)
中缀运算符可以具有从 0 到 9 的任何优先级级别,0(默认)为最低优先级,9 为最高优先级。标准基础提供了这些内置的中缀规范
infix 7 * / div mod infix 6 + - ^ infixr 5 :: @ infix 4 = <> > >= < <= infix 3 := o infix 0 before
注意,在第三行中,::和@使用infixr而不是infix。这使得它们是右结合的而不是左结合的;而3 - 4 - 5意味着(3 - 4) - 5, 3 :: 4 :: nil意味着3 :: (4 :: nil).
标识符实际上可以在它引用特定函数之前被声明为中缀
infix pow fun x pow y = Math.pow (x, y) val eight = 2.0 pow 3.0
注意,在这种情况下,中缀表示法已经在声明函数中使用。另一种方法是使用op在函数声明中,无论函数是否已被声明为中缀都适用
fun op pow (x, y) = Math.pow (x, y)
首选风格通常是不这样做,但如果声明发生在一个不确定函数是否已被声明为中缀的环境中,或者当use可能在包含此类函数声明的文件中多次调用,其中函数在定义之后被声明为中缀,这将很有用。它也可以是一种表明函数可能稍后被声明为中缀的方式,可能在另一个文件中,在这种情况下,确保解析文件的顺序无关紧要将很有用。
正如我们在类型章节中所看到的,bool
(布尔值)类型有两个值,true
和 false
。我们还看到了内置的多态相等运算符 =
,其类型为 ''a * ''a -> bool
。密切相关的是不相等运算符 <>
,其类型也为 ''a * ''a -> bool
,当 =
返回 false
时,它返回 true
,反之亦然。
<
(小于)、>
(大于)、<=
(小于或等于)和 >=
(大于或等于)运算符被重载以可用于各种数值、字符和字符串类型(但与算术运算符一样,两个操作数必须具有相同的类型)。
三个主要函数对布尔值进行操作
- 函数
not
,类型为bool -> bool
,将true
映射为false
,反之亦然。 - 中缀运算符
andalso
,类型为bool * bool -> bool
,将(true, true)
映射为true
,并将所有其他可能性映射为false
。这被称为短路运算符;如果它的第一个操作数是false
,那么它将返回false
而不评估它的第二个操作数。 - 中缀运算符
orelse
,类型为bool * bool -> bool
,将(false, false)
映射为false
,并将所有其他可能性映射为true
。像andalso
一样,它是一个短路运算符,但方向相反:如果它的第一个操作数是true
,那么它将返回true
而不评估它的第二个操作数。
布尔值的一个主要用途是在条件表达式中。此形式的表达式
if boolean_expression then expression_if_true else expression_if_false
如果 boolean_expression
评估为 true
,则评估为 expression_if_true
的结果;如果 boolean_expression
评估为 false
,则评估为 expression_if_false
的结果。与短路运算符一样,不会评估不需要的表达式。这使得条件表达式可用于创建递归函数
fun factorial x = if x = 0 then 1 else x * (factorial (x - 1))
它还允许条件副作用
if x = 0 then print "x = 0" else print "x <> 0"
注意,由于条件表达式返回一个值,因此“then”和“else”分支都需要存在,并且具有相同的类型,尽管它们不必具有特别的意义
if x = 0 then print "x = 0" else ()
函数可以由一个或多个规则组成。规则由其函数名、参数模式及其表达式组成。当函数被调用时,参数值将按自上而下的顺序与模式匹配。使用模式匹配的函数与 case 表达式非常相似。事实上,可以将任何 case 结构转换为函数。例如,这段代码
case compare(a,b) of GREATER => 1 LESS => 2 EQUAL => 3
在语义上等同于
fun case_example_function GREATER = 1 | case_example_function LESS = 2 | case_example_function EQUAL = 3; case_example_function compare(a,b);
第一个匹配的规则将得到评估并返回其表达式。这意味着如果模式是重叠的(多个模式匹配给定的参数值),则必须牢记只有第一个匹配的规则会被评估。
fun list_size (nil) = 0 | list_size (_::xs) = 1 + str_len xs;
如果对于所有合法参数值都存在一个匹配模式,则函数模式被称为完全。以下示例是不完全的。
fun list_size ([a]) = 1 | list_size ([a,b]) = 2;
任何空列表或大小>2的列表都会导致Match
异常。为了使其完全,可以添加一些模式。
fun list_size ([a]) = 1 | list_size ([a,b]) = 2 | list_size (nil) = 0 | list_size (_) = 0;
大小>2的列表将返回 0,这没有太多意义,但函数模式现在是完全的。
异常用于中止评估。有几个内置的异常可以使用raise关键字抛出,并且可以使用exception关键字定义自己的异常。异常可以通过在声明中写入of附加消息,并且使用方式与数据类型构造函数非常相似。异常可以使用handle关键字捕获。示例
exception Exc of string; fun divide (_, 0) = raise Exc("Cannot divide by zero") | divide (num, den) = num div den val zero_or_infinity = divide (0, 0) handle Exc msg => (print (msg ^ "\n"); 0)
这将打印 "Cannot divide by zero" 并由于handle子句,将 zero_or_infinity 评估为 0。
内置异常包括 Empty、Domain 和 Fail(msg)。
Lambda 表达式是可以评估为函数的表达式,而无需将函数绑定到标识符,也就是匿名函数、函数常量或函数字面量。这样的函数可以使用 Standard ML 中的fn关键字定义。这对于高阶函数特别有用,即接受其他函数作为参数的函数,例如内置的map和foldl函数。无需编写
fun add_one x = x + 1 val incremented = map add_one [1, 2, 3]
你可以简单地编写
val incremented = map (fn x => x + 1) [1, 2, 3]
注意=>运算符用于代替=。Thefn关键字可以代替fun使用,包括模式匹配甚至递归(如果rec关键字使用)。
val rec fact = fn 1 => 1 | n => if n > 1 then n * fact(n - 1) else raise Domain
请注意,通常认为使用fun关键字更好。