想了很久,还是决定写下这篇教程。因为模式匹配在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
2
3
4
In[1]:= f[x_]:= x + 1

In[2]:= f[y]
Out[2]= y + 1

其第一行出现了x_,这比较奇怪,为何不定义成 f[x]:=x+1 的形式呢?

原因是这样的:

  1. 单个字面符号的匹配是狭义的,因为只能匹配自己。(例如模式x可以匹配x,但显然不匹配y)
  2. 我们通常需要使模式可以匹配类型表达式。(例如,匹配所有整数)
  3. 因此,一个非狭义的模式往往会包含blanks (_),来匹配”anything”这个概念。
  4. 具体说来,有三种形式,分别是 _ , __, ___,后两种的作用留待后述。
  5. 实际过程中,对一个模式进行命名会更加方便,这样便可以在后文中引用它了。(例如 x_ 可以看成是一个被命名为 x 的模式)

这时再来回头看In[1]中的定义,那个 x_ 中的下划线代表了”anything”,x 只是在局部的命名,可以看成一个形参。若不加下划线,x 就变成了字面符号,它只能匹配 x 本身。

更具体地说,如果把定义改成 f[x]:= x + 1

那么 f[y] 便无法匹配到这个规则,这时Mathematica就会原封不动地返回 f[y] 本身,而非 y + 1


方便起见,Mathematica中还提供了一个函数 MatchQ ,来检测一个表达式是否匹配一个模式:

1
2
3
4
5
6
In[3]:= MatchQ[y, x_]
Out[3]= True
In[4]:= MatchQ[y, _]
Out[4]= True
In[5]:= MatchQ[y, x]
Out[5]= False

可以在Blank后加上头部信息来表示只匹配该头部的表达式,例如表示整数的Integer头部等。

1
2
In[6]:= MatchQ[42, _Integer] && MatchQ[3.14, _Real]
Out[6]= True

命名和头部过滤也可以联合起来用,关于单个Blanks的解释告一段落,下面是双Blank。


2.2 BlankSequence (__)

两个下划线组成的模式相当于单个Blanks的序列,它会匹配一个或多个Blank所能匹配的内容。

1
2
3
4
5
6
In[7]:= MatchQ[{a, b, c}, {_}]
Out[7]= False
In[8]:= MatchQ[{a, b, c}, {__}]
Out[8]= True
In[9]:= MatchQ[{a, b, c}, __]
Out[9]= True

把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
2
3
4
In[10]:= MatchQ[{1, 2, 3}, _?ListQ]
Out[10]= True
In[11]:= MatchQ[{1, 2, 3}, _?NumberQ]
Out[11]= False

这个意思是说,元素{1, 2, 3}对 ListQ 的测试返回真,对 NumberQ 的测试返回假。虽然它是一个由数字组成的列表,但它本质上还是一个列表,而非数字。

模式 _?ListQ 很有意思,它由三部分组成:第一部分是下划线,即上一节所述的Blank,它表示要匹配一个元素;第二部分是一个问号,这表示在对这个待匹配元素提问,是否具有某种性质;问号后紧接着的就是这种性质的名称,这个名称代表着一个函数,返回True或False。

ListQNumberQ 这一类函数被称为“测试函数”,一般用来测试某个表达式是否属于某种类型或具有某种性质。这类函数会以一个大写的Q来结尾,来与其他函数作区分,其中Q是Question的缩写。

这样,整个表达式 _?ListQ 的意思就清楚了,它会匹配单个列表元素;类似地,_?NumberQ 会匹配单个数字。


3.1.2 使用 _headName 的头部信息

先看下面的这个例子:

1
2
3
4
In[12]:= MatchQ[{1, 2, 3}, _List]
Out[12]= True
In[13]:= Head[{1, 2, 3}]
Out[13]= List

同样是匹配列表,还可以直接写出头部的名称来匹配。取出一个元素的头部信息可以用 Head 函数,正如In[13]所演示的那样。

这种头部的方式可以很容易的区分某些类型,例如实数和复数。在Mathematica中,实数会用 Real 头来标识,而复数则以 Complex 来表示。

下面的例子解释了这一点:

1
2
3
4
In[14]:= MatchQ[1 + 2 I, _Complex]
Out[14]= True
In[14]:= MatchQ[1 + 2 I, _Real]
Out[14]= False

为了更加精确地说明这种头部信息的用法,来举一个极端的例子:

1
2
In[15]:= MatchQ[iamahead["some", "things"], _iamahead]
Out[15]= True

这说明 _headName 这种模式会精确地匹配元素的头部信息,即使是自定义的名字,也可匹配。


3.1.3 使用任意条件表达式

这种用法可以看做 _?hasSomeAttributeQ 的推广,将其中的测试函数换成了一个任意的条件表达式。

例如,Length[#] > 2 & 就是一个算子形式的条件表达式,测试一个元素的长度是否超过2。

1
2
3
4
In[16]:= MatchQ[{a, b, c}, _List?(Length[#] > 2 &)]
Out[16]= True
In[17]:= MatchQ[{a, b, c}, _List?(Length[#] > 4 &)]
Out[17]= False

此外,头部信息和测试条件可以联合起来使用。

来把前面讲解的内容都小结一下:

范例 命名  模式 头部信息 附加条件 将会匹配
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
2
3
4
In[18]:= MatchQ[x^2, _^y_ /; EvenQ[y]]
Out[18]= True
In[19]:= MatchQ[x^2, _^y_ /; OddQ[y]]
Out[19]= False

这里的匹配模式是 _^y_ ,出现了两个Blank,这是允许的。它表示一个幂次,并且指数部分被命名为 y,而后面的 EvenQ[y] 则要求指数部分是偶数。于是整个匹配模式 _^y_ /; EvenQ[y] 就表示匹配一个指数为偶数的幂次。


/; 的条件匹配还可以用在函数定义上,用的好则可以大大简化表达式。例如,一个斐波那契数列可以被如下定义:

1
2
3
In[20]:= Clear[f]
In[21]:= f[1] = f[2] = 1;
In[22]:= f[n_] := f[n - 1] + f[n - 2] /; IntegerQ[n]

由于测试函数 IntegerQ[n] 的引入,f 不会对非整数参数进行计算。

1
2
In[23]:= {f[1.2], f[3], f[10]}
Out[23]= {f[1.2], 2, 55}

一个Fibonacci数列应该只对正整数有定义,所以需要再加一个条件:

1
2
3
In[24]:= Clear[f]
In[25]:= f[1] = f[2] = 1;
In[26]:= f[n_] := f[n - 1] + f[n - 2] /; IntegerQ[n] && Positive[n]

这样,对负数便会保持不计算的形式。


注意到该限制条件的辖域(Scope)整个表达式,可以将其辖域缩小。例如下面的表达式和上面的等价:

1
2
3
In[24]:= Clear[f]
In[25]:= f[1] = f[2] = 1;
In[26]:= f[n_ /; IntegerQ[n] && Positive[n]] := f[n - 1] + f[n - 2]

这种做法也被称为“辖域最小化”原则。