Mathematica/规则、模式、函数

引言

编辑

本章将考察Mathematica中函数的概念,也将介绍大量函数的实例。因为Mathematica的语言在很大程度上是函数式的,所以函数在这里是中心对象。又因为列表是数据结构的通用构件,任何复杂的数据结构都可以用列表组建并被快捷地处理,所以,与面向对象编程语言相比较而言,我们学习的重心就落在了函数上。

关于Mathematica中的函数与函数式编程还有很重要的一点:这一层的编程是建立在规则引擎(ruel-based engine)上的,在我看来规则比函数更加基本。这导致Mathematica中函数的概念比其他语言更深更广而且很大区别。所以,不理解规则和以规则为基础的编程技巧是很难对函数有全面掌握的。我们也会对规则有所讨论。

规则与模式

编辑

为了更好的理解Mathematica中的函数,我们需首先对模式和规则替换有很好的理解。他两好比一只圆规的两个角——模式的形式决定了规则是否被应用。

Rule、RuleDelayed、Replace、ReplaceAll

编辑

基础的规则和头部Rule

编辑

组织起一条重写规则很简单。比如,下面的这条规则会把a替换成b:

In[]:= Clear[a, b, ourrule];
       ourrule = a -> b
Out[]= a -> b

这个箭头("->")的完全形式是Rule命令。如果我们查看一下ourrule的完全形式就能发现Rule:

In[]:= FullForm[ourrule]
Out[]= Rule[a, b]

如何应用规则:ReplaceAll

编辑
我们定义的规则本身没有任何用处。Rule命令本身只是存储一条规则左值和右值的容器。Rule和引起表达式中规则替换的命令组合在一起才有用。

这个命令是ReplaceAll,完全形式ReplaceAll[expr, rules],简写形式expr/.rules。这里可以有超过一条规则被应用——把这几条规则放进平坦 的列表(如果这个列表是嵌套的话就另当别论)里。相关的例子后面会有。

首先看看我们的规则是怎么作用到表达式上的:

In[]:= Clear[f, a, b];
       f[a] /. ourrule
Out[]= f[b]
(*或者这样*)
In[]:= f[a] /. a -> b
Out[]= f[b]

如果我们的表达式更加复杂,符号a出现在多次。那么所有的a都会被替换:

In[]:= Clear[f, g, h];
       f[a, g[a, h[a]]] /. a->b
Out[]= f[b, g[b, h[b]]]

ReplaceAll在表达式上的尝试顺序

编辑

ReplaceAll对规则替换的尝试通常从较大的表达式开始,如果在一个较大的表达式上匹配,便不会再尝试其子表达式。当模式形如h[x_]时,匹配的顺序就是这样的。比如:

In[]:= Clear[a, q];
       {{{a}}}/.{x_}:>q[x]
Out[]= q[{{a}}]
现在多次引用规则,替换所有的{x_}为q[x]:

 In[]:= {{{a}}} /. {x_} :> q[x] /. {x_} :> q[x]
 Out[]= q[q[{a}]]
 In[]:= {{{a}}} /. {x_} :> q[x] /. {x_} :> q[x] /.{x_} :> q[x]
 Out[]= q[q[q[a]]]

当模式仅仅为List时,情况就不一样了:

In[]:= {{{a}}} /. List -> q
Out[]= q[q[q[a]]]

以上的行为似乎挺符合逻辑,如果真的想改变替换的顺序的话,也是有办法的。(见第5章)

规则的结合律

编辑

从上面的例子可以看出,规则是左结合的。也就是说形如expr/.rule1/.rule2的表达式是合法的。rule1会先应用到expr上,然后是rule2应用到前面的结果上。

规则的局部性

编辑

规则修改的不是原表达式,而是原表达式的拷贝,原表达式不会被改变。比如,上面例子(f[a] /. a->b)中的f[a]在单独计算时并没有发生改变:

In[]:= f[a]
Out[]= f[a]

如果一个规则和一个函数表示了相同的替换过程(比如x_:>x^2和g[x_]:=x^2),那么两者的区别是什么呢?区别是:函数定义了一个全局的规则——以后每一个输入的表达式都会被这条规则所尝试;单独的规则是局部的——只有你主动叫它应用到某个表达式上(ReplaceAll命令)时它才会起作用。这是函数和规则之间唯一的根本区别。举个例子来说我们可以用局部规则轻松模仿一个平方函数:

In[]:= Clear[f];
       {f[x], f[y], f[elephant], f[3]} /. f[z_] :> z^2
Out[]= {x2, y2, elephant2, 9}

延时规则 RuleDelayed函数

编辑

我用上面的例子来介绍两个新的概念。首先,规则的左边是模式——模式被用来确定规则应用的范围;其次,我们使用了一种新的规则(一共只有两种类型的规则,我们已经介绍了第一个)":>",完全形式为RuleDelayed:

In[]:= RuleDelayed[a, b]
Out[]= a :> b

从名字上就可以看出,RuleDelayed对应的是一种延时的规则替换——即规则的右边只有在替换确确实实发生时才计算,这个Set和SetDelayed的区别很类似。这不是偶然的,这种相似性正好体现了赋值的实质是创建全局规则。

Rule和RuleDelayed的区别

编辑

为了阐述Rule和RuleDelayed的区别,考虑这样一个问题:建立一个列表,将其中的所有元素替换成一个随机数。首先建立这样的一个表:

In[]:= Clear[sample, a, b, c, d, e, f, g, h];
       sample = {d, e, a, c, a, b, f, a, a, e, g, a}

用Rule来替换:

In[]:= sample /. a -> Random[](*在最近的版本中,随机函数被替换为RandomReal和RandomInteger,详见帮助*)
Out[]= {d,e,0.746682,c,0.746682,b,f,0.746682,0.746682,e,g,0.746682}

再来试试RuleDelayed:

In[]:= sample /. a :> Random[]
Out[]= {d,e,0.16611,c,0.45842,b,f,0.144402,0.75737,e,g,0.515928}

第一种情况中所有的数字都是一样的,而第二种是不一样的。这是因为,第一种情况中规则的右边(Random)在被应用前已经被计算了,后面的替换用的都是这个结果。而在第二种情况,Random[]在每一次规则应用是都被重新计算。为了更好的解释这一点,假设我们想把所有的a替换成{a,num},num是a已经出现的次数。我们尝试用Rule来试试:

In[]:= n = 1;
       sample /. a -> {a, n ++}
Out[]= {d, e, {a, 1}, c, {a, 1}, b, f, {a,1}, {a,1}, e, g, {a,1}}

显然,这样不灵。用RuleDelayed没准可以:

In[]:= n = 1;
       sample /. a :> {a, n++}
Out[]= {d,e,{a,1},c,{a,2},b,f,{a,3},{a,4},e,g,{a,5}}
In[]:= Clear[sample, n];

规则替换的有序性

编辑

规则列表

编辑

如果想在一个表达式上应用多条规则,把这些规则统统放在一个列表里。比如:

In[]:= {a, b, c, d} /. {a -> 1, b ->2, d ->4}
Out[]= {1, 2, c, 4}

这里要注意的是所有的规则必须放在一个一维列表(flat list)里。用嵌套的列表并不会报错,但是得到的却是从原表达式的拷贝替换来的多个结果。看看下面的例子就能明白:

In[]:= {a, b, c, d} /. {{a -> 1, b -> 2}, {c -> 3, d -> 4}}
Out[]= {{{1, 2, c, d}, {a, b, 3, 4}}}

有序性

编辑

规则替换的结果通常跟规则在列表中的顺序有关,正如下面的例子所提到的:


 In[]:= Clear[a, f];
        f[a] /. {a -> 5, a -> 6}
        f[a] /. {a -> 6, a -> 5}
 Out[]= f[5]
        f[6]

一旦某些规则成功应用到了某个表达式的一部分上的话,ReplaceAll会奔向另一个部分继续尝试替换。即使我们多次运行ReplaceAll(有一个相关的ReplaceRepeated命令),不同的顺序仍然会导致不同的结果。 这是因为前面的规则会重写表达式(的一部分)而让后面的表达式不再匹配。

我们最后的结论是规则的应用是不满足交换律的,它是有顺序的,其顺序会影响结果。作为此结论的极端例子,我们会马上构造出一个阶乘函数——如果改变其规则的顺序会导致无限循环。

规则和计算过程之间的相互影响

编辑

使用Mathematica时始终要记得我们的工作永远不是从一张空白的草稿纸开始的,而是基于联系起内置函数的众多系统规则。这些系统规则可以被操纵或者自定义,这赋予了使用这些函数的极大灵活性。一方面,你必须小心谨慎,因为新自定义规则会马上跟内置规则发生作用。上面提到的有序性会是这些相互作用及其复杂。这经常会引起出乎意料的或者“错误”的行为,很多用户就马上断言这是Bug。其实一旦你对这个系统是如何工作的有了更深入的了解,这些“Bug”是可以避免的。

规则一应用,表达式就计算

编辑

考虑一个以符号为参数的伽马函数:

In[]:= Clear[a];
       Gamma[a]
Out[]= Gamma[a]

关于Gamma函数的规则一条也没应用,式子原样返回了,因为系统根本不知道a是什么?让我们添上一条重写规则:

In[]:= Gamma[a] /. a -> 5
Out[]= 24

看,一旦a被替换成了一个数字(整数),某个内置规则就起作用了,结果算出来了。如果我们把它替换成圆周率Pi的话,因为没有一条规则会强迫Mathematica算出数值结果,我们得到的会是:

In[]:= Gamma[a] /. a -> Pi
Out[]= Gamma[Pi]

如果真的想得出数值结果的话:

In[]:= Gamma[a] /. a -> N[Pi](*N是求数值结果用的*)
Out[]= 2.28804

在这里我要强调的是:关于“是保持Gamma[5]的形式还是计算出数值结果?”这类问题的答案是很模糊的,它们是被Mathematica的“老规矩”决定的,没有通用的原则来判断会出现哪一种结果(符号形式或数值形式?)。事实上,有时我会希望Gamma[a]保持不计算的形式(符号形式)。更普遍的说,基于规则的方法的优势在于,往新情境里添加规则不需要某个“首要原则”来指导。

这并不是说Mathematica是不可预判的,而是说我们写程序的时候不应完全听命于那些“老规矩”——我们应该有“所有的表达式都可以计算成别的什么东西”的信念。如果你需要一个表达式保持不计算形式,那可得小心了;另一方面,如果想要一个表达式计算到底(比如算成数值形式),你也要确保万无一失。

计算影响规则的可应用性

编辑

现在看看这个不同的例子:

In[]:= {f[Pi], Sin[Pi], Pi^2}
       {f[Pi], Sin[Pi], Pi^2} /. Pi -> a
Out[]= {f[Pi], 0, Pi2}
       {f[Pi], 0, a2}

我们会幼稚地认为上面列表的第二项Sin[Pi]会被替换成Sin[a]而不是0, 因为我们应用了规则Pi -> a 。原因很容易理解,"/."只是个简写而已,与其等价的完全形式可以写成:

In[]:= ReplaceAll[{f[Pi], Sin[Pi], Pi^2}, Pi -> a]
Out[]= {f[a], 0, a^2}

现在让我们回忆一下Mathematica的计算策略:子表达式通常先于母表达式计算。这意味着计算过程走到ReplaceAll那里时我们的表达式早已变成Sin[Pi]的结果(0)。而0不包含Pi,所以它不再与规则匹配,替换过程不再在其身上发生。又一次,我们可以用Trace命令看到计算的动态过程:

In[]:= Trace[{f[Pi], Sin[Pi], Pi^2} /. Pi -> a]
Out[]= {{{Sin[Pi],0},{f[Pi]],0,Pi^2}},{f[[Pi],0,Pi^2}/. Pi->a,{f[a],0,a^2}}

这个例子可能给我们一种感觉——规则顺序方面的bug会导致Mathematica的不稳定。确实,很多复杂的bug跟这有关,但通常都有方法去避免它们。只要保证规则或规则列表正确地表示了实在的性质(比如数学性质)而且没有特殊的情况会导致错误结果,一般来说都会没事的。

在某些不可预测的情形下,“正确”的规则可能会导致错误的结果,于是bug出现了,但这在更传统的编程范式中也会出现。也许,真正的区别是传统的编程技巧更易于将程序限制在问题的变量空间的一个小角落,在这个小角落里正确的行为可以被预测或证明出来。我个人认为规则顺序所衍生出的复杂性(可能是无法避免的)是为非常通用的计算方法的实现所付出的代价。

规则和简单(非限制)模式

编辑

让我们给出一些关于最简单的模式的例子。

一个简单的规则和通用模式匹配策略

编辑

我们已经看到过最简单的模式了,这就是一个单独的下划线"_",完全形式是Blank[]:

In[]:= Blank[]
Out[]= _

这个模式表示任意Mathematica表达式。让我们举几个Mathematica表达式的例子:

x^y * Sin[z]

现在我们利用最简单的模式把任意的表达式替换成符号a:

In[]:= Clear[a, x, y, z, g, h];
       {x, Sin[x], x^2, x * y, g[y, x], h[x, y, z], Cos[y]} /. _ -> a
Out[]= a

这可一点也不让人兴奋。我们整个的表达式都被替换成a了!在我们继续推进之前,让我来稍微解释一下模式是怎么工作的以及为什么基于模式的替换是可能的。基于符号树的Mathematica表达式的统一表示方法是这里的要点。简单的说,当我们用一些模式与一些表达式相匹配时,我们在匹配两棵“树”。用来表示模式的树依然是合法的Mathematica表达式,但是有一些“树枝”或者“叶子”被替换为特殊的符号比如Blank[](下划线).比如:

In[]:= FullForm[(_^_) * Sin[_]]
Out[]= Times[Power[Blank[], Blank[]], Sin[Blank[]]]

如果某个表达式expr的结构和此模式一样而且expr的某一部分可以和Blank[]之类的占位符想适配,那么此模式树就和expr匹配。特别的,上面的模式就能和任何一个幂式和正弦的乘积相匹配。

检查模式是否匹配:MatchQ函数

编辑

MatchQ,这是个非常有用的命令,它可以检查一个给出的表达式是否和一个给出的模式匹配。它的第一个参数是一个表达式,第二个参数是一个模式,匹配时返回Ture否则返回False。比如:

In[]:= MatchQ[x^y * Sin[z], (_^_) * Sin[_]]
Out[]= True
In[]:= MatchQ[Exp[-x^2]^2*Sin[Cos[x-y]^2], (_^_)*Sin[_]]
Out[]= Ture
In[]:= MatchQ[x*Sin[z], (_^_)*Sin[_]]
Out[]= False

简单(非限制的)模式的匹配完全上基于语法的,而跟表达式的意义无关,理解这一点很重要。

模式标签(名称)与表达式重构

编辑

认识到特定的表达式可以与特定的模式相匹配这一点是很重要的吗,但是如果我们能访问表达式的部分,这个部分能与特定的模式相匹配,并进一步处理这些部分,这就更有用了。这称之为表达式的重构,是非常强大的模式匹配功能。比如,在上面的例子里我们可能想知道“基”是什么?指数是什么?Sin的参数是什么?为了能够进行表达式的重构,我们必须给模式的部分打上标签。这可以通过模式标签的机制做到:我们给模式的 “部分”附上一个符号看,然后这个符号就存储了与其匹配相应表达式,然后准备进行进一步的处理。下面的例子演示了如何给表达式打标签:

(base_^pwr_) * Sin[sinarg_]

模式标签不能为复合表达式,只能是真正的符号(拥有头部Symbol)。

模式标签的出现丝毫不影响匹配的结果,它只是给了我们额外的信息。然而,我们并不能从MatchQ得到这些信息,Match只是给出了匹配的结果,True或者False。我们需要一个真正的规则替换,因为这样能让Mathematica知道对这些已经匹配到的(子)表达式进行怎么样的进一步操作。比如,在这里我们仅仅把它们收集到一个列表里:

In[]:= {x^y*Sin[z], Exp[-x^2]^2*Sin[Cos[x-y]^2]} /. (base_^pwr_) * Sin[sinarg_] -> {base, pwr, sinarg}
Out[]= {{x, y, z}, {E, -2 x2, Cos[x-y]2}}

我们上面所做的只是重构的一个特例而已。我们现在可以对打上标签的部分进行任意的操作。

所以,总结如下:每当一个模式包含形如Blank[]的(还有几个这样的符号,马上会讲到)特殊符号,有时会被打上标签,被真正匹配到的表达式的相应部分就能被这些特殊符号表示。然而,不包含这些特殊符号的部分(我们例子里的Times、Power和Sin),在模式和表达式中必须以完全相同的方式被表示。

另外还有很重要的一点要提到,在模式不同部分出现的相同的模式标签不能表示不同的子表达式。也就是说相同的模式标签始终表示相同的子表达式。例如:

In[]:= MatchQ[a^a, b_^b_]
Out[]= True
In[]:= MatchQ[a^c, b_^b_]
Out[]= False

例子:拥有单一固定参数的任意函数

编辑

下面的模式能够作用于任意的函数,只要这个函数拥有唯一的参数x:

Clear[f, x];
f_[x]

这个模式可以被应用于,比如,当我们想把x替换为一个值(比如10),只要x作为一个函数的唯一一个参数。下面这个表达式列表在整节中都会被使用,用来演示各种不同的模式:

Clear[x, y, z, g, h, a]
{x, Sin[x], x^2, x*y, x + y, g[y, x], h[x, y, z], Cos[y]}

现在可以试试我们上面提到的模式:

In[]:= {x, Sin[x], x^2, x*y, x + y, g[y, x], h[x, y, z], Cos[y]} /. {f_[x] -> f[10]}
Out[]= {x, Sin[10], x^2, x y, g[y, x], h[x, y, z], Cos[y]}

替换只发生在列表的第二项。为了便于理解,可以查看一下FullForm:

In[]:= FullForm[{x, Sin[x], x^2, x*y, x + y, g[y, x], h[x, y, z], Cos[y]}]
Out[]= List[x, Sin[x], Power[x, 2], Times[x, y], Plus[x, y], g[y, x], 
h[x, y, z], Cos[y]]

可以看到只有列表的第二项包含x且只有一个参数,接下来的4个有两个参数,接着一个有三个参数,最后一个虽然只有一个参数,但是它的参数是y而不是x。

首个参数固定的任意二参数函数

编辑

现在让我们编出另外一些作用于列表其它部分的规则。首先,我们来建立一个可以作用于两个参数的函数的规则,这很简单:

In[]:= {x, Sin[x], x^2, x*y, x + y, g[y, x], h[x, y, z], 
 Cos[y]} /. {f_[x, z_] -> f[10, z]}
Out[]= {x, Sin[x], 100, 10 y, 10 + y, g[y, x], h[x, y, z], Cos[y]}

这里有个新的模式f_[x, z_], 它表示任意的二参函数,第一个参数必须是x,第二个参数任意。注意到g[y, x]没有被匹配,因为这里的x在后面,而在我们的模式中是在前面。通常来说,对于简单的模式,判断一个模式是否跟某个表达式匹配的方式是,观察模式和表达式的FullForm,看看模式和表达式是否完全一致。上面模式的完全形式是:

In[]:= FullForm[f_[x, z_]]
Out[]= Pattern[f,Blank[]][x,Pattern[z,Blank[]]]

为了更好的理解上面的匹配是如何在我们的列表中进行的,我们可以构建一个不同的规则,这个规则可以显示哪一部分被匹配了:

In[]:= {x,Sin[x],x^2,x*y,x+y,g[y,x],h[x,y,z],Cos[y]}/. {f_[x,z_]->{{"f now",f},{"z now",z}}}
Out[]= {x,Sin[x],{{f now , Power},{z now,2}},{{f now , Times},{z now,y}},{{f now , Plus},{z now,y}},g[y,x],h[x,y,z],Cos[y]}

让我们把g[y, x]的问题修好,方式是再添加一个规则:

In[]:= {x,Sin[x],x^2,x*y,x+y,g[y,x],h[x,y,z],Cos[y]}/. {f_[x,z_]->f[10,z],f_[z_,x]->f[z,10]}
Out[]= {x,Sin[x],100,10 y,10+y,g[y,10],h[x,y,z],Cos[y]}

同时处理一到三个参数的情况

编辑

注记

编辑