四、表达式的计算
四、表达式的计算 返回目录页
1、表达式
2、表达式的计算过程
3、变换规则初步
4、模式
5、变换规则和定义
6、变换规则+模式匹配
再次引用网友的话:
其实,Mathematica是一个基于规则和模式的重写系统。藏在各种炫目功能和编程形式背后的是一个精心设计的规则替换和模式匹配引擎。Mathematica中的函数是规则,变量也是规则,甚至可以说在Mathematica里变量和函数根本没有本质区别因为它们都是被附加了规则的符号而已。这在其它语言中是很难想象的事情,也正式因为这一点,很多在传统语言中难以做到的事在Mathematica都能实现。比如:在运行过程中修改函数的定义。
经过巧妙的伪装,这个重写系统能模拟出函数式风格,而且模拟地很好,rule-based编程自然也是水到渠成,过程式风格也能刚好凑合,这不能不说是很特别!
( 来源于知乎:https://www.zhihu.com/question/20324243 )
这章才进入MMA编程的核心层面:表达式与表达式的计算。
-------------------------------------------------------------------------
1、表达式
MMA处理多种不同形式的对象:数学公式、列表及图形等。
尽管它们在形式上看起来有所不同,但是MMA以统一的方式来表达它们。它们都是表达式。
在MMA中,表达式更重要的用途是保持一种结构,使得该结构能被其他函数调用。
这种结构,就是树形结构。
TreeForm[x^3 + (1 + x)^2]
我们可以观察到树形结构的图形。
当然,表达式也可以用一种缩进的文本格式呈现,上一章中我们已经看到了。
其实,代码本身,也是一种树形结构的呈现呀。不过要小心,代码可能会是这样的:
x^3 + (1 + x)^2 (这是为了人们能够阅读方便)
但是,在MMA的内部,在表达式的计算过程中,表达式以“全名”的标准格式存放:
x^3 + (1 + x)^2 // FullForm
得:Plus[Power[x,3],Power[Plus[1,x],2]]
全名表达式本身也是一种树形结构的呈现方式,内层在树叶方向,外层在树根方向。
-------------------------------------------------------------------------
2、表达式的计算过程
MMA是解释性语言,输入表达式,运行,然后得到输出。(尽管MMA具有将部分代码进行编译的功能。)
在运行过程中,即解释语言的过程中,表达式经过计算。
表达式的计算分为两种:标准的与非标准的。
非标准的包括以下等等:
x=y
不计算左边的x。
If[p,a,b]
当p为Ture时,计算a不计算b。反之则反之。
----------------------------------------
表达式的标准运算过程。
标准计算过程采用深度优先方式遍历表达式树。感性讲,就是先计算表达式的内层,然后一层层向外层。
这只是讲了表达式计算的顺序。
具体讲,表达式的计算过程,是个递归的过程:
一边在规则库中搜索规则(rule),一边进行变换(transformation),直到无规则可用为止。
规则,又叫变换规则(transformation rules)。
变换,又叫代码重写(term rewriting)。
这意味着MMA反复进行计算直到结果不再变化为止。
这是一个总体的过程描述。也是MMA的核心计算方法。
至于具体到一个简单的表达式,计算过程是:
计算表达式的头部。
依次计算表达式的每个元素。
使用与属性 Orderless、Listable 和 Flat 相关的变换规则。(因为是入门教程,这个不展开)
使用已经给出规则。(比如自定义函数)
使用内部规则。(比如内置函数)
计算出结果。
这一套计算方法,有个名称,叫:Rule-based programming
这正是MMA的特别之处,也是MMA特别强大的核心原因。
那规则到底是啥呢?
我们按这样的步骤步步细化:先讲一些简单的规则,再讲模式(模式匹配为规则服务,使变换功能更加强劲),再讲一些规则之后,然后最终使变换规则与模式匹配合璧。
-------------------------------------------------------------------------
3、变换规则初步
小金鱼对渔夫说:你要啥呢?
渔夫说:我要一套豪宅。
啥 -> 豪宅
----------------------------------------
变换规则的基本格式。
left-hand side -> right-hand side
简写为:
lhs -> rhs
lhs -> rhs // FullForm
得:Rule[lhs,rhs]
语法糖用得比较多,比较容易阅读。
规则本身也是表达式,可以“批量生产”:
Table[f[i] -> i!, {i, 5}]
----------------------------------------
运用变换规则。
expr /. lhs->rhs 对 expr 运用变换规则
expr /. {lhs1->rhs1,lhs2->rhs2],...} 将一列变换规则用于expr的每一项
x + y /. x -> 3
得:3 + y
x + y /. {x -> a, y -> b}
得:a + b
还可以这样,得到一个表:
x + y /. {{x -> 1, y -> 2}, {x -> 4, y -> 2}}
得:{3, 6}
Solve 和 NSolve 等函数的返回值是一列规则,每个规则代表一个解:
Solve[x^3 - 5 x^2 + 2 x + 8 == 0, x]
得:{{x -> -1}, {x -> 2}, {x -> 4}}
重复替代运算 //. 使得规则被反复使用直到表达式不再变化为止:
x^2 /. {x -> 2 + a, a -> 3}
x^2 //. {x -> 2 + a, a -> 3}
注意两者区别:
expr /. rules 在 expr 的每一项中用变换规则一次
expr //. rules 重复使用规则直到结果不再变化为止
----------------------------------------
计算顺序。
对于Rule函数,即lhs -> rhs,在运用于表达式时,计算顺序为:
先计算表达式,再计算规则左边与右边,然后,表达式中与规则左边匹配的部分,均被规则右边替换。
举个栗子:
Table[x, {3}] /. x -> RandomReal[]
再用Trace跟踪一下:
Table[x, {3}] /. x -> RandomReal[] // Trace
依次计算,然后表达式中的x被随机数替换。可以看到,三个随机数是相等的。
还有一种RuleDelayed函数,基本格式是:
lhs :> rhs
lhs :> rhs // FullForm
当它运用于表达式时,计算顺序就不同了:
先计算表达式,再计算规则左边(右边不计算!),然后,表达式中与规则左边匹配的部分,均被未被计算过的规则右边替换。
因为右边在替换前未被计算过,所以叫Delayed
Table[x, {3}] /. x :> RandomReal[]
Table[x, {3}] /. x :> RandomReal[] // Trace
所以可以看到,得到的三个随机数是不同的。
总结一下:
lhs->rhs 给出规则后就计算 rhs
lhs:>rhs 使用规则时计算 rhs
-------------------------------------------------------------------------
4、模式
小金鱼对渔夫说:你要啥呢?
渔夫说:我要比一套豪宅好的东西。
_啥 -> 比一套豪宅好的东西
----------------------------------------
上面讲的变换规则,其实功能很弱,为啥呢?
因为以上的变换规则,都太单调、太死板了,要求完全匹配。
有了模式(patterns)的概念之后,匹配的范围就剧烈增大了。
模式纯粹是为了变换规则服务,使规则变换时匹配功能大大增强了。
以前,渔夫只能要一套豪宅。现在,有了模式匹配功能,渔夫可以要比一套豪宅好的任何东西了。
这就是模式的意义所在。也是MMA功能强劲的根本原因。
(有些书在讲规则之前就讲模式,这样容易让人看得一头雾水。所以在这里我们开门见山:模式这一生,只为规则而活。)
模式的基本格式。
模式至少包含以下三种下划线(blank,又翻译成空位)的一种:
_ a single blank (有时被称作通配符(wild card)模式,因为它可以匹配任何表达式)
__ a double blank
___ a triple blank
_ 可以匹配任何一个,表达式。
__ 可以匹配,一个或一个以上表达式所组成的,序列。
___ 可以匹配,零个或零个以上表达式所组成的,序列。(这里的逗号看似语法错误,其实是故意的。。)
所谓序列(sequence),就是经常写在[]内作为函数参数,或者写在{}内,作为表元素的东西,用逗号分隔。
这样说有点啰嗦,换种说法:一个序列由若干逗号分隔的表达式构成。
模式块可以单独使用,也可以命名。
比如命名为x_,则可以在变换规则的右端引用它。
----------------------------------------
可以把下划线放在表达式中的任何位置,来形成匹配模式。这是相当灵活的,请看:
f[n_] 变量名为 n 的 f
f[n_,m_] 变量名为 n 和 m 的 f
x^n_ 指数为 n 的 x 的幂
x_^n_ 任何次幂的表达式
a_+b_ 两个表达式的和
{a1_,a2_} 两个表达式组成的列表
f[n_,n_] 有两个相同变量的 f
判断能否匹配,主要用两个函数:
MatchQ[x, x^n_]
得:False
虽然,x的1次方等于x,但x的内部形式并不是x的1次方,所以无法与x^_n匹配。
Cases[{3, 4, x, x^2, x^3}, x^n_]
得:{x^2,x^3}
Cases返回能够匹配的表中元素。这样很容易看出,x与x^n_不匹配,但x^2与x^3是匹配的。
这样看就更清楚:
{3, 4, x, x^2, x^3} /. x^n_ -> n
得:{3, 4, x, 2, 3}
Cases[{{a, b}, {}, {1, 0}}, {p_}]
Cases[{{a, b}, {}, {1, 0}}, {p__}]
Cases[{{a, b}, {}, {1, 0}}, {p___}]
观察输出,三种下划线各匹配什么序列,就很清楚。p_可以匹配任何表达式,但不能匹配序列。
----------------------------------------
模式中的表达式类型限制。
可以通过头部来区分不同"类型"的表达式。
模式中,_h 和 x_h 表示具有头部 h 的表达式。
例如,_Integer 表示任何整数,而 _List 表示任何列表。
x_h 具有头部 h 的表达式
x_Integer 整数型
x_Real 实数型
x_Complex 复数型
x_List 列表型
x_Symbol 符号型
{a, 4, 5, b} /. x_Integer -> p[x]
得:{a, p[4], p[5], b}
整数4、5与x_Integer匹配,所以进行了转换。a、b不是整数,保持原样。
----------------------------------------
限制模式。
Mathematica 中提供了对模式进行限制的一般方法。
这可以通过在模式后面加 /;condition 来实现。
此运算符 /; 可读作"斜杠分号"、"每当"或"只要",其作用是当所指定的 condition 值为 True 时模式才能使用。
pattern/;condition 当条件满足时,模式才匹配
lhs:>rhs/;condition 当条件满足时,才使用规则
lhs:=rhs/;condition 当条件满足时,才使用定义
fac[n_ /; n > 0] := n!
fac[6] + fac[-4]
得:720 + fac[-4]
n>0限制了-4与n_匹配,所以变换没有发生。
Cases[{3, -4, 5, -2}, x_ /; x < 0]
得:{-4, -2}
x<0限制了正数3、5的匹配。
可以将 /;condition 放在 := 定义域或 :> 规则后告诉 MMA 只有当指定的条件满足时才能使用此定义或规则。
但要注意 /; 不能放在 = 或 -> 规则后,因为这些是立即被处理的。
Table[x, {3}] /. x -> RandomReal[] /; 1 == 2
Table[x, {3}] /. x :> RandomReal[] /; 1 == 2
观察结果,很有意思。第一句也能运行,只是把RandomReal[] /; 1 == 2部分都当成了规则的右边。
第二句,condition条件限制了变换。变换没有发生,所以表是原样的。
在 MMA 中有一类函数去测试表达式的性质。这类函数后有一个 Q,表明它们在"提问"(Question?)。
IntegerQ[expr] 整数
EvenQ[expr] 偶数
OddQ[expr] 奇数
PrimeQ[expr] 素数
NumberQ[expr] 任何数
NumericQ[expr] 数字型
{2.3, 4, 7/8, a, b} /. (x_ /; NumberQ[x]) -> x^2
仅对数字求平方。
还可以这样用:
MatchQ[{1, 2, 3}, _?ListQ]
True
MatchQ[{1, 2, 3}, _?NumberQ]
False
{2.3, 4, 7/8, a, b} /. x_?NumberQ -> x^2
这样写把/;舍弃了,代码更紧凑,也更易读了。
----------------------------------------
有多种供选方案的模式。
选择模式(alternatives)是指,由几个独立的模式构成的模式,这一模式与表达式匹配,只要构成它的任一独立模式与这个
表达式匹配即可。
MatchQ[x^2, x^_Real | x^_Integer]
也可以这样用:
{a, b, c, d} /. (a | b) -> p
a或者b,均变换成p。
-------------------------------------------------------------------------
5、变换规则和定义
MMA的符号结构支持广义的赋值概念,您可以使用MMA的模式定义指定任意类型表达式的转换。
一般我们将带等号的表达式(赋值语句),称为“定义”。
所有定义,包括自定义函数,及内置函数,本质上也是变换规则(有时或叫重写规则)。
表达式的计算过程,就是不断重写的过程,直到无重写规则可用为止。
这节我们来看各种定义。
----------------------------------------
在第三节中,我们讲了关于表达式的变换规则。还有两种重写规则:
内置函数(built-in function)和用户自定义重写规则(user-defined rewriting rule)。
用户自定义重写规则可以使用Set函数和SetDelayed函数来建立,基本格式是:
lhs = rhs
lhs := rhs
rand1[x_] := RandomInteger[{1, x}]
rand2 = RandomInteger[{1, 8}]
可以看到,第一行没输出,第二行有输出。那是因为Set函数被输入时,右边马上进行计算,而有输出。
而SetDelayed函数被输入时,右边是不计算的,要等到被调用(重写)时,才进行计算。
(*
rand1[x_] := RandomInteger[{1, x}];
很多时候,可以在结尾加上分号,增加阅读性。经常结尾有分号的是不输出的语句。
对于定义本身来说,最后的分号加不加都无所谓——反正右边不计算,加了也无输出。
而对于立即赋值如:
rand2 = RandomInteger[{1, 8}]
要看具体情况。加不加分号右边都计算了。加了分号无输出,不加反之。
*)
当我们要自定义一个函数时,并不要求左边与右边被求值。所以SetDelayed函数通常被用来写一个函数的定义。
当我们对一数值进行声明(declaration)时,我们并不打算对左边进行计算,只是想给数值一个比较简便的别名。这正是Set函数可以为我们做到的。
现在我们理解了,自定义函数也好,内置函数也好,都是重写规则。这与C、Pascal等语言中的函数实现截然不同。
我们可以从一般的函数概念中解放出来,以写重写规则的方式来写自定义函数,可以写出很多非常灵活的自定义函数。比如:
h[{x_, y_}] := x^y
h[{2, 3}]
得:8
也就是说,函数的参数,完全是可以形式多样的。
而对于别名,我们也知道了,这是一个重写规则。无非是一般数值在形式上比较长,取个别名方便使用。
lis = {1, 2, 3, 4, 5, 3, 2, 4, 3};
Apply[Plus, lis]
除 Module 和 Block 等一些内部结构外,在 Mathematica 中的所有赋值都是永久的。
若没有清除或改写它们,在MMA的同一个进程中所赋值保持不变。
赋值的永久性意味着使用时要特别慎重。一个在使用MMA时,常犯的错误是在后面使用 x 时忘记或误用了前面 x 的赋值。
所以当你发现结果极不正常时,第一个要想到的是用Clear函数清除别名。
总结一下:
lhs=rhs (立即赋值) 赋值时立即计算 rhs
lhs:=rhs (延时赋值) 每次需要 lhs 时计算 rhs
----------------------------------------
特殊形式的赋值
i++ i 加 1
i-- i 减 1
++i 先给 i 加 1
--i 先给 i 减 1
i+=di i 加 di
i-=di i 减 di
x*=c x 乘以 c
x/=c x 除以 c
这估计是向C语言学的,在形式上一模一样。
i+=di 相当于i=i+di,其余类推。
----------------------------------------
定义带标号的对象
可以把表达式 a[i] 当作一个"带索引"或"带下标"的变量。
这与C或Pascal中的数组不是同一概念。
有时候变量多时,这种方式可以提高程序阅读性。当然,可以有更多的用处。。
下标不一定是数字,可以是任何表达式。比如:
a[one] = 1
a[two] = 2
a[i]=value 改动变量的值
a[i] 调用变量
a[i]=. 删除变量
?a 显示定义过的值
Clear[a] 清除定义过的值
----------------------------------------
修改内部函数。
这个,属于插播。。
在MMA中可以对任何表达式定义变换规则。不仅可以定义添加到 Mathematica 中去的函数,而且还可以对MMA的内部函数进行变换,于是就可以增强或修改内部函数的功能。
这一功能是强大的,同时也具有潜在的危险。。。
因为危险,还是自己去看帮助文档吧:)
-------------------------------------------------------------------------
6、变换规则+模式匹配
这里分析两个较为复杂的栗子。
先做深呼吸七次。(为啥是七次?因为七是神奇数字:))
----------------------------------------
what1[x_List] := x //. {a___, b_, c___, d_, e___} /; d <= b :> {a, b, c, e}
然后,凝视这一串语法糖数分钟,想一想这个函数是干啥的?
想不出来么?先别看下面的分析,去做一个随机整数表,作为参数。然后调用一下这个函数,看看得出什么结果?
看不出来?那产生随机整数表n个,观察输出n个,然后应该就清楚了。
----------------------------------------
what2[L_List] := Map[({#, 1}) &, L] //. {x___, {y_, i_}, {y_, j_}, z___} -> {x, {y, i + j}, z}
同理,深呼吸七次。。。
同理,观察输出n次。。。
----------------------------------------
Table[i, {30}] /. i -> 刷屏一页
得:
{刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页,
\
刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页,
\
刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页, 刷屏一页}
----------------------------------------
第一题分析:
lis := Table[i, {8}] /. i :> RandomInteger[{0, 8}]
what1[x_List] :=
x //. {a___, b_, c___, d_, e___} /; d <= b :> {a, b, c, e}
Table[li = lis; {li, what1[li]}, {10}]
观察结果,可以看到一个递增序列:凡是比前面任何一个数字小的数字,都会被删除。
//. 不断变换,直到不能变换为止。
/; d <= b 这部分,是个条件,对 {a___, b_, c___, d_, e___} 的模式匹配进行限制。
凡是还存在着符合匹配的,就不断进行以下转换:
{a___, b_, c___, d_, e___} /; d <= b 转换成 {a, b, c, e}
{a序列,b元素,c序列,d元素,e序列}中,如果d元素不大于b元素,则表在进行重写转换时,把d元素删除了。
因为这个计算过程要不断进行,所以要用//.进行不断变换。
理解这段程序的关键,在于模式匹配:包括表中序列的匹配与元素的匹配。
----------------------------------------
第二题分析:
what2[L_List] := Map[({#, 1}) &, L] //. {x___, {y_, i_}, {y_, j_}, z___} -> {x, {y, i + j}, z}
lis := Table[i, {8}] /. i :> RandomInteger[{0, 8}]
Table[li = lis; {li, what2[li]}, {5}]
观察结果知道,这是个统计连续数个数的程序。(不进行排序是因为这样记录了原来的顺序,很容易还原)
Map[({#, 1}) &, L]
Map函数,是把无名函数 ({#, 1}) & ,分别作用于表L中的每个元素。
这一步相当聪明,相当于起始工作完成。
得表再进行模式匹配的规则转换:
{x___, {y_, i_}, {y_, j_}, z___} -> {x, {y, i + j}, z}
左元素相同的,连续排列的: {y_, i_}, {y_, j_}
合并成:{y, i + j}。左元素不变,右元素累加。这下明白为啥开始的时候右元素为1了吧?
因为转换是不断进行的,所以用//. ,直到再也没有匹配,再也不能转换为止。
理解这个程序的关键,又是模式匹配。
因为表是MMA中的常用数据结构,所以对表进行这类模式匹配的变换操作还是比较实用的。
程序还可以这样编写?是啊,这是MMA的独特之处。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
有人说,MMA就是个大玩具。确实如此。
内置函数那么多,这些函数,在运行时,是可以修改功能的!
通过模式匹配,规则变换,可以把函数功能发挥到极致。
看完这一章,我们知道了:在MMA中,过程式编程和函数式编程,都是模拟出来的,MMA的核心编程方法是
Rule-based programming(基于规则的编程方法,简略说,就是:规则编程)
++++++++++++++++++++++++++++++
扩展阅读:知乎上的,Wolfram Language话题精华。
Top