本文原题为 题目简析+标准答案+指导评分标准,用于面试作业阅卷工作,在此一并发出,如有叙述语气上的不妥之处,还请多多见谅。
一
1-1
3
-3
24/49 或 0.4897
49/24 或 2.041
这种还做错的直接拒了吧。
1-2
2 * (3 + 5)! => * 2 ! (+ 3 5)
4 + 7!! => + 4 ! ! 7
((1 + 3)! * 6)! => ! (* ! (+ 1 3) 6)
注意参顺序不可交换,括号加不加都算对,不过不能加错。
1-3
减法-
、除法/
:左结合
乘方^
:右结合
因为2 ^ 3 ^ 2 = 512 = 2 ^ 9 = 2 ^ (3 ^ 2)
1-4
标准答案:
如果将所有运算符看作函数,首要的先决条件是所有函数的参数数量必须是固定的。其次,另一个重要的条件是这里只涉及算术运算,因此我们知道所有数在运算树中代表的节点,一定是叶子节点。这两点加在一起,就能让我们从运算树的前序遍历重构出整棵树(哪怕存在非二元的运算符)。
更普遍地,只要我们可以直接通过一个节点的值判断出其的子节点个数(对于叶子节点来说,子节点个数为0),我们就一定能从前序遍历重构出整棵树。当然这并不是一个必要条件,不过已经足够回答这个问题本身了。
评判标准:
以上述形式回答问题需要(二叉)树和遍历顺序的前置知识,评判时应非常注意思路相同但并未使用术语的描述形式。
固定的参数数量来自维基Polish Notation页面中的描述:
It does not need any parentheses as long as each operator has a fixed number of operands.
由于本文中并没有提及到变长参数的存在性(除了 习题 5-1 中的flow-left
使用nil
结尾的参数列表模拟了变长参数的行为),一般读者难以想到其作为条件之一。因而期望中的合适答案应当包括运算树一定为叶子节点,或者其类似的表述形式。
当然,如果能够得到 子节点数可通过节点值确定 或者更强的结论则更好。
请注意本题和 习题 2-4 之间的关联与区别。
面试结束后的更新:
不知道是由于时间原因没来得及仔细思考还是题目的方向不明确(也许都有),在实际交上来的作业中几乎很少有答到这个要点的人。(好吧我承认就是题目方向不明确)不过在面试过程中进行引导的话,比较优秀的读者基本都能够推出这一结论。
我相信肯定有人当时想破头都不知道我想问什么,结果看了答案又觉得答案很没意思的——嘛,就当我语文老师好了ゴゴゴゴゴ。(怨念满满)
二
2-1
mulAdd := a. b. c. + a b * c
做不出来可以直接拒了。括号可加可不加。
2-2
(fmul2 add1) 3
的计算过程略。
fsquare
的定义:
fsquare := f. n. (^ (f n) 2)
fsquare := f. n. (* (f n) (f n))
两种答案均可,如果回答了后一种,可以在提问 习题 3-4 时进行询问,“如果f
有副作用时会如何”。
也可预先提取square
函数,然后再fsquare
中引用,代码略。
2-3
标准答案:
or := (a. b. (a true b))
题解:
not
函数可被翻译为:如果a
为true
,返回false
,否则返回true
。
and
函数可被翻译为:如果a
为true
,返回b
,否则返回false
。
因此or
函数也可被翻译为:如果a
为true
,返回true
,否则返回b
。
评判标准:
本题难度较大,实测中鲜有读者能够很快得到标准答案。
本题而言,合适的思路是从or
的真值表出发,逐变量进行讨论,得出or
的行为为“如果a
为true
,返回true
,否则返回b
”。在此基础上,利用布尔值的条件判断性质给出答案a true b
。
为了能够得出此答案,读者需要理解的概念还有:
true
和false
同时作为布尔值和逻辑运算函数存在,即它们的类型既是B
又是B -> B -> B
。这一点的理解难度颇高,尤其对于接触过其他编程语言的读者来说。对于这些读者来说,更容易理解的概念会是and := if a (if b true false) false
。- 逻辑运算始终发生在布尔值范畴中,因此对于
not := a. a false true
来说,无需考虑a
不是true
或false
的情况。根据设计,这种情况没有任何意义,布尔值必须通过if
函数转换为非布尔值。理解这一点对于接触过其他编程语言、拥有一定类型系统概念的读者来说格外困难,因为这里的类型定义是隐含的、从用例中推得的。没能给出更多例子、做出更好的解释,是我的失误。
在标准答案以外,出现以下这些情况的,可以进行酌情评判。
- 通过德摩根律(
a || b = !(!a && !b)
),利用and
和not
定义or
,可能经过了0次或多次归约 - 未能理解布尔值同时可做为逻辑运算函数,从而未能将
if a b c
归约为a b c
或类似情况。 - 未能理解函数式编程中“功能相同即为同一函数”的思想,从而未能将
a true false
归约为a
或类似情况。
如果时间允许,可以考虑在面试环节使用上述解题思路,对面试者进行引导,以冀导出正确的答案。
另提供一种完全基于真值表的解题思路:
or := if a (if b true true) (if b true false)
=> a (b true true) (b true false)
=> a true b
也请面试官尽量掌握本题的解题思路,以应对一切可能的提问。
面试结束后的更新:
其实在面试开始之前就有人问到过这个问题了,这里统一说一下。
实际上,本文中所使用的类型系统看起来是一个隐藏了算术实现细节的无类型lambda演算,表现在这道题里面就是,我们实际上并没有(也做不到)限制传给一个函数的参数类型。很多人都问到过这个问题就是,比如我写一个+ 1 (n. n)
会得到什么?我们知道加法+
应该接受两个数作为参数,然而(n. n)
显然是个函数,这个表达式显然是不合法的——当时基本上都是这样回答的,也就是说,无法归约得到一个(数)值的表达式就是非法的表达式,它不会得到任何东西,也没有什么“返回值”。
但这个说法实际上是不完整的。如果我们真的是一个无类型lambda演算,那么就会出现一个严重的问题,那就是演算中是不存在错误的。原因在于,在无类型lambda演算中,连数值本身都是通过邱奇数进行定义的,比如2
代表的是f. x. f f x
。在这种意义上,数也是函数,任何函数永远需要至少一个参数,因此任何两个东西放在一起总能得到另一个东西,表达式永远不会出错。
这个毛病在存在逻辑运算的时候尤为严重,我们完全可以写出诸如and true (n. n)
这样的式子,它看起来很怪,看不出有什么含义在里面,它违背了我们定义and
的初衷。但诡谲得是它的确能够返回一个(n. n)
作为返回值,有什么理由拒绝你写出这种奇怪的式子呢?
解决方法之一是引入类型,比如我们要求加法+
总是接受两个数,如果参数中有一个不是数就直接中断计算,顺便把错误信息打出来。比如true
和false
的类型是Bool
,and
、or
的类型是Bool -> Bool -> Bool
,if
的类型是Bool -> A -> A -> A
。
这串充满着->
的符号告诉我们一件事:and
和or
的参数只能是true
或false
,而不是1
或者(n. n)
,同时,true
和false
的类型就是Bool
,不能当作函数来使用,必须要借助if
才能进行条件判断、“转换”为其他类型的值。注意这里的类型定义都是“对外的”,仅供使用时进行类型判断,在“内部使用”(比如if
的定义中的cond. then. else. (cond then else)
)时仍然可以把true
或false
当作函数使用。当然,在一门现代的函数式编程语言(如Haskell)中,这一操作往往会使用特殊的语法(如模式匹配),来避免对于“内部”和“外部”的界定问题。
于是现在,我们终于可以说,and true (n. n)
是一个不合法的表达式,因为(n. n)
是一个A -> A
,而不是一个Bool
。
2-4
阅读本题答案前,请首先阅读 习题 1-4 的答案。
等我恶补一波类型系统回来再说。
面试结束后的更新:
在出这道题的时候我确实想的比较简单,只要拿一个高阶函数出来就能制造歧义,甚至自己也写了一个简单的例子来“证明”括号的必要性——
——然后就被打脸了。等我自己开始写这份标准答案的时候,才发现这个问题远远没有想象中的这么简单,我自己写的例子也不靠谱。幸好,我最近恶补了一波范畴论和类型系统,总算能够大概回答一下问题了。好,那我们开始正题。
首先答案是需要。反例如:
comp := f1. f2. x. f1 f2 x
czero := n. n
csucc := n. + 1 n
cadd1 := f. comp csucc f
cmul2 := f. comp f f
swap := f. g. g f
swap cadd1 cmul2 cadd1 czero 0
= cmul2 cadd1 cadd1 czero 0 / cadd1 cadd1 cmul2 czero 0
= 4 / 2
不过在充分理解这个问题之前,我们必须明确什么是歧义。
在前缀表达式和中缀表达式的分别中,我们已经看到,同一个表达式可以有不同的表达形式。反过来说,一个数字和符号组成的串,也可以代表不同的含义,比如从左到右或从右到左读