• Home
  • About
    • refraction-ray photo

      refraction-ray

      Blog of thoughs and archive of experience

    • Learn More
  • Posts
    • All Posts
    • Tags Archive
    • Posts Archive
  • Projects
  • RSS

Mathematica 中的 OwnValues, DownValues, UpValues

22 Jan 2019

  • 引言
  • 四种 Values
  • UpValues 的几种应用
    • 模拟字典和对象
    • 重载系统函数
  • Mathematica 的运行机制
  • 简单总结
  • Reference

引言

关于 mathematica 中的这几种 Value 的区别和理解,参考1已经说的比较清楚了。本文算是对这一内容,以及 mathematica 内部一些文档2的一个笔记,当然也有一些关于这几种 Value 更直观的理解,同时我也会谈到 UpValues 一些可能的应用。贯穿其中的还有对 mathematica 运行时的本质以及编程范式的比较思考。

四种 Values

要理解这些 value 的区别,首先要知道 mathematica 的运行机制,是完全基于表达式,规则匹配这些形而上的东西。所谓的函数,变量等等在其他编程语言中所熟悉的事物,只不过是 mathematica 通过这些匹配机制巧妙编织的幻觉。而四种 Values 实际上就直指了这一幻觉的核心。建议读者熟悉 _,__ 等 pattern 的概念和语法来阅读本文。

首先是 OwnValues,这是用来构造变量幻觉的主力。例如:

a = 1;
OwnValues[a]
(* {HoldPattern[a] :> 1} *)
a
(* 1 *)

mma 实际做的事情,是读到 a 之后,寻找到了 OwnValues 中的替换规则,然后将 a 这个字符串,替换成了 1 这个字符串。可以证明这一点,戳破 a 是对应一个占据内存地址变量的例子是 a[1],mma 会给出 1[1] 的结果,这正说明了 mma 的运行时机制是按照一定规则无脑的字符串替换,变量只是大多数情况下这个替换带来的幻觉。

然后是 DownValues,这是用来构造函数幻觉的主力。例如:

b[x_] := 1
DownValues[b]
(* {HoldPattern[b[x_]] :> 1} *)
b
(* b *)
b[1]
(* 1 *)

因此 DownValues 专门用来匹配后跟中括号的 Symbol 的替换。DownValues 可以有多个规则,例如 :

b[x_] := 1;
b[x_, y_] := 2
DownValues[b]
(* {HoldPattern[b[x_]] :> 1, HoldPattern[b[x_, y_]] :> 2} *)

对于不同的模式(这里 mma 也不太有本质上的参数概念,其运行时所看到的只是 b 后边的字符串有没有逗号而已),可以采用不同的替换规则,类似函数的重载。注意 OwnValues 优先于 DownValues, 因此一旦 b 本身也有定义,那么 b 作为函数的身份就会被屏蔽掉。也即无法重载出 b 返回1, b[1] 返回2的效果,而只会是 b[1] 返回 1[1]。

关于这里变量幻觉定义中用的 =, 而函数幻觉定义中用的是 :=,千万不要以为这是 mathematica 定义语法的一部分。两者都可以用在变量和函数定义中,其唯一的区别是变量替换是否延迟到需要时在执行。这一方面只需考察 c=Random[];{c,c} 和 c:=Random[];{c,c} 的返回就知道我在说什么了。正因为在传统的语言中,变量大多是即时运行,函数都是调用时运行,因此 mma 中这样定义比较符合传统的幻觉,但不是必须的,还需按情况选用 = 和 :=。

为了充分说明,mma 根本没有函数的概念,参数也只是字符串,请参考以下例子。

g[x_ + y_] := gp[x, y]; 
g[x_ y_] := gm[x, y]
g[Unevaluated[2 + 3]]
(* gp[2,3] *)
g[Unevaluated[2  3]]
(* gm[2,3] *)

可以看到,mma 中传统函数的概念完全消解了,连所谓逗号分隔的变量也全是幻觉,有的只是中括号里的内容作为整体的模式匹配而已。

你甚至可以直接操作这些 Values 的 list,请看以下例子。如果能理解这个例子,那么说明对于 mathematica 运行的机制就算入门了。

d[x_, y_] := 1;
d[x__] := 2;
d[1,2]
(* 1 *)
DownValues[d] = Reverse[DownValues[d]];
d[1,2]
(* 2 *)

由于 UpValues 没有一个传统的幻觉对象,所以是在其中比较难理解的一个。语意上,既然 UpValues 是 DownValues 的反义词,那么 DownValues 是根据符号后面中括号内的内容结合来匹配替换,UpValues 则反其道而行之,根据符号及包含其的中括号对应的函数(外层函数),来决定和重定义内层函数的行为。这样说或许太抽象了,还是直接看例子。

g/: f[g]:=1;
f[g]
(* 1 *)
f[a]
(* f[a] *)
f[g[a]]
(* f[g[a]] *)
Clear[g]
g/: f[g[__]]:=1;
f[g]
(* f[g] *)
f[g[a]]
(* 1 *)
f[g[a,b]]
(* 1 *)

当然,和 DownValues 一样,一个符号也可以定义多条 UpValues 的规则。这一 TagSet 也即 /: 的语义是指将其后的替换规则赋值给前边的符号的 UpValues 或 DownValues 或 OwnValues。其中前边符号(上例中是 g)处在规则函数内部时,即为 UpValues,而如果写为 g/: g[f]=1,则事实上改变了 DownValues,其语义和 g[f]=1 等价,因此不必使用这种 syntax。OwnValues 同理, g/: g=1 和 g=1 等价。

UpValues 还有 UpSet 这一只属于自己的原语。即 f[g[__]]^:=1。这种方式和 TagSet 区别不大,唯一的一点区别可以参考3。

说白了 UpValues 就是从内部魔改,考虑到 mma 运行机制是嵌套表达式从里到外,先分析各符号的 OwnValues,再依次分析 UpValues,DownValues。请看以下例子:

Clear[f, g, h];
f[x_] = x;
f[g] ^= 1;
Print[f[g]];
Print[f[h]];
(* 1  h *)

最后我们可以通过 ?Symbol 的 shortcut 快速查看一个变量绑定的所有 Values 的替换规则。

此外还有一个所谓的 SubValues,这货 mma 连文档都没。其对应的是形如 f[a][b] 这样的定义。

f[a][b][c] := 1;
SubValues[f]
(* {HoldPattern[f[a][b][c]] :> 1} *)
d=f[a][b]
(* f[a][b] *)
d[c]
(* 1 *)

UpValues 的几种应用

模拟字典和对象

比如我们可以做一个存公式的字典。

square /: Area[square] := s^2;
square /: Circum[square] := 4 s;
circle /: Area[circle] := \[Pi]r^2;
circle /: Circum[circle] := 2 \[Pi]r;

Area[square]
(* s^2 *)

我们发现这东西和 python 的 dict 很像,这里我们相当于构造了 Area 和 Circle 两个 dict,不过底层上这和函数一样,依然是一种幻觉而已,本质上还是符号匹配与替换。

当然 mathematica 有了 association 这种数据结构之后,有些需求也可以考虑用 association 实现更加直观。

更进一步的,考虑到 Head 在 mathematica 运行和分析时的重要性,我们可以构造一套面向对象的系统。用 Head 来代表对应的内容。这一内容我将结合下小结的重载函数一起来看。

重载系统函数

如果我们想要重构系统函数,使得对于一些输入(比如我们新定义的“类”),其函数行为有所不同,那么我们就有两条路径,一条是通过 Unprotect@func 来解除系统函数 func 的保护,从而重构。细节可以参考4中的解释。这一方案缺点很多,比如修改系统函数行为可能带来不可控的影响,同时由于 mma 的运行机制,函数的每个 DownValues 都会依次评估。比如我们想重载加号,也就是重载 Plus,那么我们每次做加法,这一新添加的 DownValues 都会被评估,拖慢了运行时间。毕竟所有做的加法里,只有很小一部分和我们新定义的类有关。

反之,如果我们把这种重构行为绑定在我们新定义的类上,行为就安全了许多。这就需要 UpValues。请参考以下代码,我们实现了一种加法是乘法的数。

SNum /: SNum[a_] + SNum[b_] := a*b;
SNum[1] + SNum[2]
(* 2 *)

注意到 + 对应了外层函数是 Plus ,因此 UpValues 定义符合之前的语法。按照这种思路推广下去,利用 UpValues 我们可以实现较好的封装性并模拟很多面向对象的特征。

Mathematica 的运行机制

每一句命令,mathematica 都看成是一个表达式。而表达式最重要的特征是具有 Head 属性。因此 mathematica 通过从里向外,先匹配 OwnValues, 然后匹配 UpValues 再匹配 DownValues的顺序,依次完成对相应子表达式 pattern 的替换,直到结果没有任何定义的替换规则匹配为止。

注意上面所述的这个运行过程里,根本就没出现传统的函数,内存空间等概念。不严格的说,按照传统的编程语言的观点,mathematica 的运行机制都是在来回操纵字符串而已。为了更清楚的理解这一点,我们从一个加法函数来看静态语言 C++,动态语言 Python 和 Mathematica 运行机制的区别。

/* myadd.cpp */
#include <iostream>
int myAdd(int a, int b){
    return a+b;
}
int main(){
    int a=1, b=2;
    std::cout<<myAdd(a,b);
}

对于 C++ 代码需要提前通过 gcc 等编译工具生成机器语言,也就是二进制文件。里面就包含了最底层的 cpu 如何操纵内存和进行计算的信息。例如通过 g++ -S myadd.cpp,生成的汇编文件中,可以找到 addl -8(%rbp), %esi 这样的句子,对应了 cpu 层次的加法运算。程序运行时,main 中遇到 myAdd,可以直接跳转到 myAdd 函数二进制存放的内存地址,然后 cpu 按照对应的指令执行操作即可。这里的加法函数,只能计算整数的加法,如果传入实数就会报错,因为占据的内存大小不同,加法计算的 cpu 指令也不同。要想实现统一名称的函数也能计算实数的加法,静态语言就只能求助于重载。但重载的本质,也不过是编译时,编译器自动起了不同名字自动连接而已,运行时的行为都不会发生改变。某种程度上,重载也只能算是静态语言的一个语法糖而已。而想要将函数作为其他函数的参数,则只能通过传递函数指针(也即函数地址),或者函数对象的方式实现。

对于 Python,事情更复杂一些。

"""
myadd.py
"""
def myAdd(a,b):
    return a+b

if __name__ == "__main__":
    print(myAdd(1,2))

当执行上面这段代码,也即输入 python myadd.py 时,实际上分成了两个阶段。第一阶段是编译,将所有代码都转化为字节码。与 cpp 转化为机器码不同,这里的字节码是 python 解释器可以识别的,自己定义的一套虚拟机机器码。这一字节码可以通过 python -m dis myadd.py 查看,函数对应的字节码内容如下。

0 LOAD_FAST                0 (a)
2 LOAD_FAST                1 (b)
4 BINARY_ADD
6 RETURN_VALUE

python 执行的第二阶段是运行,由 python 解释器开始逐一调用并执行字节码。python 的灵活之处在于,字节码对应的执行函数是高度抽象和多态的。比如上面的 BINARY_ADD,并不一定只能执行整数的加法,也可以执行实数的加法,甚至对象的加法。由于 python 一切皆对象,因此只要加号左边的对象定义了 __add__ 方法,就可以执行加法。这就造就了动态语言的灵活性和鸭子类型。同时在 Python 里由于函数不过是定义了 __call__ 方法的对象,因此可以轻松的作为其他函数的参数出现。

而对于 mathematica,运行的机制和以上语言完全不同。

(* myadd.nb *)
myAdd[x_,y_]:=x+y;
Print[myAdd[1,2]];

看起来函数定义只是和 python 略有语法区别而已,但实际上底层大不相同。当执行第一句函数时,mathematica 只是在 myAdd 这一变量的 DownValues 列表里加了一个替换规则。也即

DownValues[myAdd]
(* {HoldPattern[myAdd[x_, y_]] :> x + y} *)

注意到,这一过程并为生成任何字节码之类的东西,这里实质上根本就不存在函数的概念。当 mathematica evaluate 第二句的 myAdd 函数时,mathematica 将其理解成一个表达式,从里往外找 Head,第一个是 myAdd,然后 mma 会依次尝试该名称是否有 OwnValues,UpValues,DownValues 对应的规则。找到 DownValues 对应的替换规则后,按照模式匹配,将 myAdd[1,2] 这个整体替换为 1+2 这个整体。注意这一操作,完全类似于传统语言中对字符串的操作,myAdd 完全没有所谓函数的概念。mma 继续分析表达式,1+2 是 Plus[1+2]的简写,最终得到 3 这一结果。在 mma 中,函数参数的类型概念就更弱了,因为从头到尾,执行的都不过是一些基于规则的字符串替换而已。那么函数作为其他函数参数就是天然的事情,甚至连 python 中将函数打包封装成一等公民对象这一实现都不需要。

简单总结

由以上从几种 Values 发散出的对 mathematica 运行机制和底层的思考来看,mathematica 并不是所谓的函数式编程。准确的说 mma 应该算是一种多范式编程。其变量,函数,对象,都是建立在字符串替换规则上的一套假象,其一等公民并不是函数,而是表达式本身。这一本质特性在众多的编程语言中是无比独特的(利用不好也可能是无比慢的)。

Reference

  1. what-is-the-distinction-between-downvalues-upvalues-subvalues-and-ownvalues ↩

  2. 详见 tutorial/AssociatingDefinitionsWithDifferentSymbols,tutorial/TheStandardEvaluationProcedure,tutorial/ManipulatingValueLists ↩

  3. TagSet vs. UpSet ↩

  4. Do people actually use UpValues ↩



mathematicapythoncpp