-
Notifications
You must be signed in to change notification settings - Fork 0
表达式求值器使用教程
欢迎使用表达式求值器,它支持对算术表达式求值,以及进行简单的编程操作,但尚不支持复杂的编程操作。
让我们开始吧:
最基础地,求值器支持对加减乘除运算表达式求值,按照 README 上说的构建项目之后,执行 node dist/main repl
进入 REPL 环境,然后我们可以对表达式求值:
请你分别尝试:
1 + 1;
1 + 2 * 3 / 4;
3 + 4 + -2 + 5 % 3;
分号 ;
是分隔开各个表达式的符号,分号亦代表当前表达式的输入结束,输入分号后,再敲一次回车键,即可触发操作系统刷新缓冲区,促进输入内容在求值管道中的流转。
乘法运算符和乘法运算符具有更高的结合优先级,而单个(前缀)取负符号比取余数符号 % 的优先级更高,取余数符号 % 的优先级比乘除号的优先级高。
我们也可以通过括号来强制优先级:
例如,对 3 + 4 * 5;
和 (3 + 4) * 5;
求值将得到不一样的结果。
And
, Or
, Not
符号提供了基本的布尔值运算功能,<
, <=
, >
, >=
, ==
, !==
提供了数和数之间的比较功能,If
提供了条件求值功能。
If[True, a, b];
的求值结果为 a
;
试一试下面这一段程序,它就利用了条件求值功能来寻找一个非空列表的最大值:
myList = RandomInteger[{1, 20}, 4];
Let[
max[lst_] := If[
Length[lst] == 0,
Nothing,
Reduce[
lst,
Function[x_, y_, If[y > x, y, x]],
First[lst]
]
],
max[myList]
];
您无需理解系统内部是如何实现的,只需知道:
f[x_, y_] := x + y;
这条语句定义了一个函数,基本语法是
函数名标识符[变量a_, 变量b_(, ...)] := 表达式
g[x_, y_] := f[x+y, x]
也是一个函数,这样定义的函数我们可以简单地称之为「普通函数」。
有时,我们更关心函数的计算方式而不是函数本身的名字,
举例:
Function[x_, y_, z_, x + y + z][1,2,3];
是匿名函数的一个应用(Application),其中 Function[x_, y_, z_, x + y + z]
定义了一个三个参数的匿名函数,它的输出结果是这三个参数的算术和,而后紧接着 [1, 2, 3]
表示对这个匿名函数的一个「调用」(Apply)操作。
如果我们需要描述一个计算法则,但是不想给这个计算法则起一个专门的名字的话,匿名函数是有用的。
下列表达式定义了一个匿名函数,它的作用是让调用者传入的参数自己与自己相乘并返回结果:
Function[x_, x * x]
你可以定义匿名函数后立即开始计算:
Function[x_, x * x][12];
也可以将匿名函数与一个标识符「绑定」在一起,然后通过这个标识符来调用这个函数:
squareFn = Function[x_, x * x];
squareFn[101];
匿名函数也可以作为函数参数,例如,将一个 C 语言风格的 compare 函数传入排序函数中作为比较器:
mySort[lst_, compareFn_] := If[
Length[lst] == 0,
{},
Let[
first = First[lst],
rest = Rest[lst],
lhs = Filter[rest, Function[x_, compareFn[x, first] <= 0]],
rhs = Filter[rest, Function[x_, compareFn[x, first] > 0]],
ListJoin[mySort[lhs, compareFn], { First[lst] }, mySort[rhs, compareFn]]
]
];
mySort[RandomInteger[{1,1000}, 40], Function[x_, y_, y - x]];
在这里,由于我们传入 mySort
函数的比较器是 Function[x_, y_, y - x]
, 所以,最终的排序输出结果会是倒序排列的,也就是说大的数排在前面,小的数排在后面。
系统当前的实现支持递归函数,递归函数就是在函数体表达式中出现了对自己的调用的表达式。你可以通过精心定义一个递归函数,来实现传统指令式编程 (Imperative Programming) 中的控制流。
下面我们定义一个求和函数,它的功能是计算前 10 个正整数的和,并且,定义好之后就调用它:
sum[n_] := Let[
sumIter[sum_, count_, max_] := If[count > max, sum, sumIter[sum + count, count + 1, max]],
sumIter[0, 0, n]
];
sum[10];
你也可以将以上内容保存为一个后缀名为 .wl
的脚本文件中,例如 sumIter.wl
或者 sum.wl
, 然后通过运行
node dist/main run sum.wl
来执行这个脚本。
我们还可以定义斐波那契函数:
Let[
fib[1] = 1,
fib[2] = 1,
fib[n_] := fib[n-1] + fib[n-2],
fib[7]
]
Let
符号用来创建临时作用域,因此在 Let
中给出的定义在 Let
表达式求值完毕后就会被系统丢弃,但是像刚才的 sum
函数,由于是定义在 Let
外边的,所以是永久的,而 Let
里边的 sumIter
则是临时的。
输入下列表达式可以定义一个排序函数:
sort[lst_] := If[
Length[lst] == 0,
{},
Let[
first = First[lst],
rest = Rest[lst],
ListJoin[
sort[Filter[rest, Function[x_, x <= first]]],
{ first },
sort[Filter[rest, Function[x_, x > first]]]
]
]
];
通过 sort[{3,18,9,2,6}];
来调用它,可以看到列表升序排列后的结果。
Seq
函数可以用来创建包含等量递增的整数的列表:
Seq[10];
Seq[2, 6];
Seq[1, 10, 2];
Seq[-10, 10];
Seq[-10, 10, 4];
请逐个尝试上列表达式。
RandomInteger
可以用来创建包含随机整数的列表:
RandomInteger[{ 3, 4 }, 5];
返回的列表中的整数的值是在一个闭区间里面随机取的,其中 3 是闭区间的下确界,4 是闭区间的上确界,如果用户输入的下确界大于上确界,则系统会交互这两个参数的角色,原来的上确界变为下确界,原来的下确界变为上确界,5 是产生的随机数的列表的长度,所以这条表达式返回 5 个随机整数,取值在 [3, 4].
Map
函数可以用来实现对数组元素的批量操作:
Map[{a, b, c}, f];
上列表达式中的 a
, b
, c
没有定义也没关系,求值器遇到无定义的符号时会原样返回,例如 a
这个符号没有定义,那么当我们输入 a;
得到的返回还是 a
.
上列表达式的返回结果是 { f[a], f[b], f[c] }
, 也就是对列表中的每个元素「应用」了 f
操作后得到的运算结果(保持原有顺序)。
Reduce
函数的功能类似于「折叠」,它可以用来「折叠」一个列表:
Reduce[{a, b, c}, f, init]; (* 等于 f[f[f[init, a], b], c] *)
我们的自测例程就用到了 Reduce
, 当你要对多个布尔表达式做 And
(或者 Or
)运算的时候:
举几个例子:
Reduce[{ True, True, False }, And, True] (* 等于 False *)
Reduce[{ True, True, True, True }, And, True] (* 等于 True *)
Reduce[{ False, True, False, False }, Or, False] (* 等于 True *)
道理跟 Reduce
的定义是基本一样的。
Reduce
还能用来求和:
Reduce[Seq[10], Function[x_, y_, x + y * y], 0]; (* 平方和 *)
Reduce[Map[Seq[10], Function[x_, x * x]], Plus, 0]; (* 平方和,同上 *)
Reduce[Seq[10], Function[x_, y_, x + y * y * y], 0]; (* 立方和 *)
Filter
可以用来过滤数组的元素,留下那些符合条件的,例如:
筛选出 Seq[10]
中的奇数:
Filter[Seq[10], Function[x_, x % 2 == 1]];
筛选出 Seq[10]
中的偶数:
Filter[Seq[10], Function[x_, x % 2 == 0]];
下列程序筛出 100 以内的素数:
primeFilter[lst_] := If[
Length[lst] == 0,
{},
Let[
first = First[lst],
rest = Rest[lst],
ListJoin[
{ first },
primeFilter[
Filter[
rest,
Function[x_, x % first !== 0]
]
]
]
]
];
primes[n_] := If[n < 2, {}, primeFilter[Seq[2, n]]];
primes[100];
在我们实现的表达式求值器中,没有变量,只有替换规则和表达式,Pattern 本身也是用表达式表示的。
a = RandomInteger[{ 1, 10 }];
这条语句会被翻译为:
Assign[a, RandomInteger[{ 1, 10 }]];
系统看到该表达式的 Head 是 Assign, Assign 这个符号被标记为了「非标准求值符号」,于是系统进入非标准求值程序,系统中有一条针对 Assign[_, _] 表达式的求值规则:先求值右边的值,也就是 RandomInteger[{ 1, 10 }], 再将得到的结果和左边的值也就是 a 做关联,将这个绑定关系「a -> 这次对 RandomInteger[{ 1, 10 }] 的求值结果」加入替换规则库,下一次我们再输入 a;
就可以得到 a
这个 Pattern 绑定的值。
b := RandomInteger[{ 1, 10 }];
这条语句会被翻译为 AssignDelayed[b, RandomInteger[{ 1, 10 }]]
, 系统会简单地将 b
这个 Pattern 与 RandomInteger[{ 1, 10 }]
关联,所以之后我们每一次对 b
这个表达式求值,系统都会执行一次 RandomInteger[{ 1, 10 }]
。
f[x_] := RandomInteger[{ 1, 10 }];
会被翻译为 AssignDelayed[f[Pattern[x, Blank[]]], RandomInteger[{ 1, 10 }]]
, 系统在求值这条表达式的时候系统会在替换规则库中加入一条规则:
f[Pattern[x, Blank[]]] -> RandomInteger[{ 1, 10 }]]
下次加入我们尝试对 f[3]
或者 f[a]
或者 f[x]
求值都会被 f[Pattern[x, Blank[]]]
这个 Pattern 匹配,系统紧接着就会对替换规则的右端也就是 RandomInteger[{ 1, 10 }]]
求值。注意:f[]
不会被 f[Pattern[x, Blank[]]]
匹配。
Let
符号提供了临时局部作用域的使用,在 Let 表达式中,只有最后一个子表达式会被真正求值,第 1 个到第 n-1 个子表达式的 Assign 或者 AssignDelayed 表达式不会被求值而是会被添加到一个临时作用域中。
举例:
Let[a = 1, b = 2, f[x_, y_] := x + y, f[a, b]];
返回 3, 也就是 f[a, b], 也就是 a + b, 也就是 1 + 2, 对 f[a, b]
及其展开后的表达式的求值都是在这个 Let 创建的局部作用域中进行的。
Let 表达式求值完毕后,我们再尝试求值 a;
或者 b;
或者 f[a, b];
都会发现没有定义(或者是 Let 之前的定义),如果没有定义系统会原样返回它们。
Let 表达式的 Assign(或者 AssignDelay)子表达式是逐级生效的,第 n-1 个先生效,然后是第 n 个,如下表达式:
Let[a = 1, b = a + 1, c = b + 1, c + 1];
的求值结果是 4.
下列程序定义了一系列局部变量用来演示快速排序的实现:
Let[
sort[lst_] := If[
Length[lst] == 0,
{},
Let[
first = First[lst],
rest = Rest[lst],
lhs = Filter[rest, Function[x_, x <= first]],
rhs = Filter[rest, Function[x_, x > first]],
sortedLhs = sort[lhs],
sortedRhs = sort[rhs],
middle = { first },
result = ListJoin[sortedLhs, middle, sortedRhs],
result
]
],
sort[RandomInteger[{1, 1000}, 40]]
];