想了很久,还是决定写下这篇教程。因为模式匹配在Mathematica太重要了,但这东西的确比较难描述清楚,下面我将尽我所能力图清晰地展现Mathematica中的模式匹配机制,部分内容参考自《An Introduction to programming with Mathematica》第六章,喜欢直接阅读英文材料的可以直接看这一节。
当然,以下均假定读者有基本的Mathematica基础。如果你以前从未接触过这个语言,建议你至少先看一下这篇文章(Mathematica中的四种括号,这篇至少是图文并茂,比本文全是代码要好一点)。
1. 一切皆是规则代换(Rule)
当你在Mathematica中写下诸如 x = 4 这样的语句时,规则代换实际上已经发生了。一般的语言会把它(x)解释成一个变量,然后赋予其值为 4 。但是在Mathematica中,却完全不是这个样子,上面的语句实际上定义了一条规则,这规则是说:任何情况下只要出现字面符号 x ,就将其替换为4。
虽然这两种解释在大多数情况下都是等价的,但两者还是有微妙的差别。
2. 模式(Pattern)
2.1 Blank (_
)
下面定义了一个叫 f 的函数,它的返回值是把参数+1。
1 | In[1]:= f[x_]:= x + 1 |
其第一行出现了x_
,这比较奇怪,为何不定义成 f[x]:=x+1
的形式呢?
原因是这样的:
- 单个字面符号的匹配是狭义的,因为只能匹配自己。(例如模式x可以匹配x,但显然不匹配y)
- 我们通常需要使模式可以匹配类型表达式。(例如,匹配所有整数)
- 因此,一个非狭义的模式往往会包含blanks (_),来匹配”anything”这个概念。
- 具体说来,有三种形式,分别是
_
,__
,___
,后两种的作用留待后述。 - 实际过程中,对一个模式进行命名会更加方便,这样便可以在后文中引用它了。(例如
x_
可以看成是一个被命名为x
的模式)
这时再来回头看In[1]中的定义,那个 x_
中的下划线代表了”anything”,x
只是在局部的命名,可以看成一个形参。若不加下划线,x
就变成了字面符号,它只能匹配 x
本身。
更具体地说,如果把定义改成 f[x]:= x + 1
,
那么 f[y]
便无法匹配到这个规则,这时Mathematica就会原封不动地返回 f[y]
本身,而非 y + 1
。
方便起见,Mathematica中还提供了一个函数 MatchQ
,来检测一个表达式是否匹配一个模式:
1 | In[3]:= MatchQ[y, x_] |
可以在Blank后加上头部信息来表示只匹配该头部的表达式,例如表示整数的Integer头部等。
1 | In[6]:= MatchQ[42, _Integer] && MatchQ[3.14, _Real] |
命名和头部过滤也可以联合起来用,关于单个Blanks的解释告一段落,下面是双Blank。
2.2 BlankSequence (__
)
两个下划线组成的模式相当于单个Blanks的序列,它会匹配一个或多个Blank所能匹配的内容。
1 | In[7]:= MatchQ[{a, b, c}, {_}] |
把In[7]和In[8]作比较,就会发现两种模式的不同之处:前者只能匹配单个元素,后者可以匹配多个元素。而In[9]则比较tricky,实际上是匹配到了整个列表,而整个列表会被视为1个元素。
2.3 BlankNullSequence (___
)
三个下划线组成的模式,可以看成Blank的推广,它可以匹配零个或多个元素。
三种Blank模式可以整理成如下表格:
模式名称 | 符号 | 作用 |
---|---|---|
Blank[] | _ |
匹配单个元素 |
BlankSequence[] | __ |
匹配一个或多个元素 |
BlankNullSequence[] | ___ |
匹配零个、一个或多个元素 |
3. 条件模式匹配 (Conditional pattern matching)
某些情况下,需要对被匹配的元素形式上提出一些要求,例如对实数参数和对复数参数采用不同的策略,这就需要用到条件模式匹配。
3.1 使用附加形式
3.1.1 使用 _?hasSomeAttributeQ 的测试形式
先看下面两个例子:
1 | In[10]:= MatchQ[{1, 2, 3}, _?ListQ] |
这个意思是说,元素{1, 2, 3}对 ListQ
的测试返回真,对 NumberQ
的测试返回假。虽然它是一个由数字组成的列表,但它本质上还是一个列表,而非数字。
模式 _?ListQ
很有意思,它由三部分组成:第一部分是下划线,即上一节所述的Blank,它表示要匹配一个元素;第二部分是一个问号,这表示在对这个待匹配元素提问,是否具有某种性质;问号后紧接着的就是这种性质的名称,这个名称代表着一个函数,返回True或False。
像 ListQ
、NumberQ
这一类函数被称为“测试函数
”,一般用来测试某个表达式是否属于某种类型或具有某种性质。这类函数会以一个大写的Q来结尾,来与其他函数作区分,其中Q是Question的缩写。
这样,整个表达式 _?ListQ
的意思就清楚了,它会匹配单个列表元素;类似地,_?NumberQ
会匹配单个数字。
3.1.2 使用 _headName 的头部信息
先看下面的这个例子:
1 | In[12]:= MatchQ[{1, 2, 3}, _List] |
同样是匹配列表,还可以直接写出头部的名称来匹配。取出一个元素的头部信息可以用 Head
函数,正如In[13]所演示的那样。
这种头部的方式可以很容易的区分某些类型,例如实数和复数。在Mathematica中,实数会用 Real
头来标识,而复数则以 Complex
来表示。
下面的例子解释了这一点:
1 | In[14]:= MatchQ[1 + 2 I, _Complex] |
为了更加精确地说明这种头部信息的用法,来举一个极端的例子:
1 | In[15]:= MatchQ[iamahead["some", "things"], _iamahead] |
这说明 _headName
这种模式会精确地匹配元素的头部信息,即使是自定义的名字,也可匹配。
3.1.3 使用任意条件表达式
这种用法可以看做 _?hasSomeAttributeQ
的推广,将其中的测试函数换成了一个任意的条件表达式。
例如,Length[#] > 2 &
就是一个算子形式的条件表达式,测试一个元素的长度是否超过2。
1 | In[16]:= MatchQ[{a, b, c}, _List?(Length[#] > 2 &)] |
此外,头部信息和测试条件可以联合起来使用。
来把前面讲解的内容都小结一下:
范例 | 命名 | 模式 | 头部信息 | 附加条件 | 将会匹配 |
---|---|---|---|---|---|
a_ |
a | _ | 无 | 无 | 一个任意表达式 |
_?ListQ |
无 | _ | 无 | ListQ | 一个列表 |
__Number |
无 | __ | Number | 无 | 一个或多个数字 |
n_?(IntegerQ && Positive) |
n | _ | 无 | IntegerQ && Positive |
一个正整数 |
x_List?(Length[#] > 2 &) |
x | _ | List | Length[#] > 2 & |
一个至少有3元素的列表 |
3.2 使用 /; condition 缩小范围
当一个被命名的模式后面紧跟着 /; condition
, 并且 condition
中包含了模式中出现的命名时,那么仅当 condition
返回True时这个匹配才可能成功。
一般来说,使用 /; condition
可以缩小一个模式匹配的范围。
用一个简单的例子来说明这一点:
1 | In[18]:= MatchQ[x^2, _^y_ /; EvenQ[y]] |
这里的匹配模式是 _^y_
,出现了两个Blank,这是允许的。它表示一个幂次,并且指数部分被命名为 y
,而后面的 EvenQ[y]
则要求指数部分是偶数。于是整个匹配模式 _^y_ /; EvenQ[y]
就表示匹配一个指数为偶数的幂次。
/;
的条件匹配还可以用在函数定义上,用的好则可以大大简化表达式。例如,一个斐波那契数列可以被如下定义:
1 | In[20]:= Clear[f] |
由于测试函数 IntegerQ[n]
的引入,f
不会对非整数参数进行计算。
1 | In[23]:= {f[1.2], f[3], f[10]} |
一个Fibonacci数列应该只对正整数有定义,所以需要再加一个条件:
1 | In[24]:= Clear[f] |
这样,对负数便会保持不计算的形式。
注意到该限制条件的辖域(Scope)整个表达式,可以将其辖域缩小。例如下面的表达式和上面的等价:
1 | In[24]:= Clear[f] |
这种做法也被称为“辖域最小化”原则。