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]}