运算符文法
*表达式的运算符表示
考虑一个数学式 2^5/7! ,或者加上圆括号 (2^5)/(7!) ,它的表达式写作
Divide[Power[2, 5], Factorial[7]]
实际上这里的圆括号是可省略的, 2^5/7! 这种写法通常被认为是使用了一种 运算符优先级语法 。其中 / ! ^ 都是所谓运算符,而 2 3 7 3! 3!^7 1/ 3!^7 都是表达式。
定义一种运算符时,须指定其一些属性。对于上面的例子,这些属性如下表:
运算符 |
元数 |
运算符位置 |
优先级 |
结合性 |
|---|---|---|---|---|
/ |
2 |
中缀 |
1 |
左结合性 |
^ |
2 |
中缀 |
2 |
左结合性 |
! |
1 |
后缀 |
3 |
(无此属性) |
这些属性会以下面的方式决定会得到怎样的表达式:
!的元数为1表明要结合附近的1个表达式,而后缀表明它将结合前面的表达式。/的元数为2表明要结合附近的2个表达式,而中缀表明它将结合前后2个表达式。^也一样。看上去
7可能先和!结合,也可能先和/结合。但!的优先级3大于/的优先级1,所以3先被!抢到手。这时得到2^5/(7!)。结合性用于判断同优先级运算符的争抢赢家。如
1/3/5到底是(1/3)/5还是1/(3/5)呢?/的左结合性指明,应让位于左边的/成为争抢的赢家。
对于元数超过2的函数,则需要更多的设计(多元运算符、选项等),此处暂且不论。
用音调标记谓词的运算符
前文已解释如何用嵌套函数来作为复合句的模型,而良好定义的运算符文法可以表达任何嵌套表达式。接下来将给出一个方案,它对一个谓词进行音调标记来得到一个运算符。
自动填充
在一般的运算符优先级语法中,运算符不具备表达原子表达式(如前文的苏格拉底 soke )的能力。
本方案允许为一个谓词添加标记,使其第1或2个参数缺失(或者说插槽空置)时自动被一个变量(匿名,需要时可命名)自动填充。至于变量如何参与语义解释,此处不限定、可作进一步设计。一个典型的做法是设计一个1元函数,其副作用是对传入的变量进行一个量化的声明。
空气没毒
kote1[
koqi1[
fola1[]
],
metu1[
pila1[]
],
]
其中:
fola1[x] := 声明 x 被全称量化并返回 xkoqi1[x] := 断言「x 是空气」并返回 xmetu1[x] := 断言「x 没毒」并返回 xpila1[x] := 声明 x 与本句中的唯一一个其他自动填充的匿名变量是语义等价的,并返回 x。kote1[{expr1, ret1}, {expr2, ret2}] := 断言「若 expr1 中的命题为真则 expr2 中的命题为真」并返回 ret1
此处量化的作用域默认是整个句子,由于只有一个量词这不是问题。需要明确作用域的情况,可考虑lambda表达式(坑,计划在后文写)。
此外这个句子用到的定义未免太过复杂,最好的办法还是将这种模式封装在词典中。如定义「P的模型蕴含Q的模型」这样的高阶谓词(如Eberban的 mao )。
插槽交换
本方案允许为一个谓词添加标记,使其第1或2个插槽的位置交换。如,若对
suma[x, y] := 声明「x 小于 y」
添加此标记就得到相应的「大于」概念的谓词。
允许的运算符类型
所有一元运算符强制为后缀运算符,所有二元运算符为中缀。
音调标记
声调1 |
声调2 |
谓词元数 |
插槽交换 |
自动填充的插槽 |
返回值 |
运算符元数 |
|---|---|---|---|---|---|---|
\ |
\ |
1 |
原型 |
无 |
1 |
1 |
/ |
/ |
1 |
原型 |
1 |
1 |
0 |
\ |
/ |
2 |
原型 |
2 |
2 |
1 |
/ |
\ |
2 |
原型 |
1 |
1 |
1 |
— |
/ |
2 |
原型 |
无 |
1 |
2 |
— |
\ |
2 |
逆序 |
无 |
1 |
2 |
/ |
— |
2 |
原型 |
无 |
2 |
2 |
\ |
— |
2 |
逆序 |
无 |
2 |
2 |
注意仅支持其中一个插槽的自动填充,且自动填充的变量必须是返回值。
注意虽然在预期的设计中一个谓词应有确定的元数,本方案仍然用标记区分了不同元数的谓词,以确保无需词典数据库亦可还原表达式。
余下的一种音调组合是 -- ,这种词不表示运算符而标记处理运算符的函数,这些函数表达式的解析在通常的运算符结合之前处理,遵循波兰表示法。由于波兰表示法的解析需要知道元数,这种标记一般适合普适性较强的,如常用的高阶函数。最简单的情况是一元的高阶函数的情况,这时它可以视为对一类最高优先级的前缀运算符的支持。
现在看一个例句
甲小于12且是最小的。
它翻译为
téfú súmā fású māxī sùmà
其中用到的谓词定义如下
fasu[x] := 将变量x记为「甲」tefu[x] := 断言「x 是 12」suma[x, y] := 断言「x 小于 y」高阶函数
maxi[P][x] := 断言「在 x 所属的关联于 P 的偏序集合中 x 是一个极小元 」。(约定其处理运算符后,保留音调所标记的属性。这个约定的问题是无法利用逆序的信息了。)
这些运算符的属性列在下面:
运算符 |
谓词元数 |
插槽交换 |
自动填充的插槽 |
返回值 |
运算符元数 |
|---|---|---|---|---|---|
téfú |
1 |
原型 |
1 |
1 |
0 |
súmā |
2 |
原型 |
无 |
2 |
2 |
fású |
1 |
原型 |
1 |
1 |
0 |
māxī sùmà |
1 |
原型 |
无 |
1 |
1 |
讨论
这里为了规则的简单整齐,可能会导致一些音调模式不适当地过多出现。
返回值的机制擅长将变量向上层(根节点)传递,但无法简单地在不同分支间传递。不过求值机制本身就是从深到浅,无结构预处理、深到浅、深到浅这三步估计已经够喝一壶了。如果目标是一个可由程序分析的语法,可能注定没特别好的办法。
一维的文字序列中的运算符不擅长处理度超过2的顶点的问题没有改变。
支持二元谓词逆序的目的有两个。一是为表达的灵活性(初版设计的灵活性实际上比这个还要强,标记需要大概两倍的状态数);二是高阶谓词如果不需要用到运算符的信息,比如若
māxī[P]的处理结果强制为一个\\,\\型的运算符,那么就不需要用到运算符本身的声调标记,那么其中包含的逆序信息可以用来区分「极大」和「极小」。