跳到内容

标准 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 + 43.0 + 4.0都是有效的(前者类型为 int,后者类型为 real)——但类型之间没有隐式提升。因此,像这样的表达式3 + 4.0是无效的;必须编写3.0 + 4.0,或者real 3 + 4.0(使用基本函数real3转换为3.0).
  • 整数除法使用特殊运算符表示div而不是斜线/;后者仅用于实数除法。(由于类型之间没有隐式提升,因此像这样的表达式3 / 4是无效的;必须编写3.0 / 4.0或者real 3 / real 4。)还有一个取模运算符mod;例如,十七除以五是三余二,所以17 div 5317 mod 52。(更一般地说,如果 q 是一个正整数,那么d = p div qm = 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(其中 fx 作为参数并返回一个接受 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(布尔值)类型有两个值,truefalse。我们还看到了内置的多态相等运算符 =,其类型为 ''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 结构转换为函数。例如,这段代码

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 表达式

[编辑 | 编辑源代码]

Lambda 表达式是可以评估为函数的表达式,而无需将函数绑定到标识符,也就是匿名函数、函数常量或函数字面量。这样的函数可以使用 Standard ML 中的fn关键字定义。这对于高阶函数特别有用,即接受其他函数作为参数的函数,例如内置的mapfoldl函数。无需编写

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关键字更好。

Let 表达式

[编辑 | 编辑源代码]
华夏公益教科书