最近接近接觸到函數(shù)式編程的范式,深感它實(shí)在是非常有用。其最突出的特點(diǎn)是No side effect, no variables, no loops。在使用函數(shù)式編程的程序中,可以不包含一個(gè)任何一個(gè)循環(huán)和變量,同時(shí)無(wú)附帶效應(yīng)也使得出Bug的幾率低了不少,調(diào)試起來(lái)也更加簡(jiǎn)單。這就使得編程的人更能集中精力思考如何解決問(wèn)題而不是操心怎么用計(jì)算機(jī)實(shí)現(xiàn)。而Mathematica是老牌的科學(xué)計(jì)算軟件,雖然冠以科學(xué)計(jì)算之名,但實(shí)際上它能處理的問(wèn)題非常多,并不僅限于科學(xué)。同時(shí)它內(nèi)建了豐富的函數(shù)庫(kù),在解決問(wèn)題時(shí)也更靈活。個(gè)人認(rèn)為完全可以取代python之類的編程語(yǔ)言。雖然python也有眾多的函數(shù)庫(kù),但最大的問(wèn)題是它們?cè)陲L(fēng)格上并不統(tǒng)一,參考文檔也參差不齊,在學(xué)習(xí)上就有比較大的困難。
而Mathematica原生支持函數(shù)式編程,與豐富的庫(kù)函數(shù)強(qiáng)強(qiáng)結(jié)合,簡(jiǎn)直是學(xué)習(xí)生活的不二之選。在Mathematica中的函數(shù)式編程風(fēng)格有幾個(gè)特點(diǎn):
萬(wàn)物皆表達(dá)式
Mathematica中的所有語(yǔ)句都是表達(dá)式,那我們就可以把多個(gè)表達(dá)式整合到一個(gè)表達(dá)式中進(jìn)行計(jì)算。所以用C++寫出來(lái)的多個(gè)語(yǔ)句,在Mathematica中可以只用一個(gè)表達(dá)式完成。不過(guò)這樣可能會(huì)帶來(lái)可讀性上的問(wèn)題,因此可以適當(dāng)?shù)貙?duì)表達(dá)式進(jìn)行拆分,寫成多個(gè)。
任何數(shù)據(jù)結(jié)構(gòu)都是表
表在Mathematica中居于十分核心的地位,因?yàn)槿魏我环N數(shù)據(jù)結(jié)構(gòu)都可以等價(jià)為一個(gè)多維表,那么對(duì)表進(jìn)行計(jì)算和操作也就是十分自然的了。Mathematica對(duì)表運(yùn)算進(jìn)行了特別的優(yōu)化,因此建議多使用表。在對(duì)表的運(yùn)算中,Map是一類核心的函數(shù),事實(shí)上在任何一種函數(shù)式編程或類似的語(yǔ)言都能找到它的身影,諸如R語(yǔ)言的Apply,Matlab的Arrayfun這樣的函數(shù)其在思想上和Map是一樣的,就是將函數(shù)f應(yīng)用于表中的每一個(gè)元素。
舉個(gè)例子,我們想尋找一100以內(nèi)的所有質(zhì)數(shù),用函數(shù)式編程的思想來(lái)做,我們就可以先生成一個(gè)1-100的整數(shù)表A,然后寫一個(gè)函數(shù)f判斷某一個(gè)數(shù)是否為質(zhì)數(shù),接著將這個(gè)函數(shù)Map到表上,從而得到一個(gè)長(zhǎng)度為100的True和False組成的表B,接著從表A中提取表B對(duì)應(yīng)位置為True的元素。
Map函數(shù)的正式語(yǔ)法為:
Map[f,expr] or f/@expr
其中的expr就是所要映射的表。同時(shí)如果有的表有多個(gè)層次,那么可以指定映射函數(shù)到哪一個(gè)層次的元素,即Map[f,expr,levelspec]。
對(duì)于剛開始的問(wèn)題,我們用原生的PrimeQ作為判斷函數(shù)f,那么就可以用PrimeQ/@Range[1,100],得到了表B,其中Range[1,100]表示生成從1到100的表。接著要判斷哪些位置上的元素為True,此時(shí)需要用到Position函數(shù),Position[PrimeQ/@Range[1,100],True]就表示表B中為True的元素的位置,然后用Extract[expr,list]提取expr中的由list指定的位置上的元素。
將以上程序?qū)懺谝黄鹁褪?/p>
Extract[Range[1,100],Position[PrimeQ/@Range[1,100],True]]
就會(huì)得到{2,3,5...}這樣一個(gè)100以內(nèi)所有質(zhì)數(shù)的表。
注意到上式中出現(xiàn)了兩次Range,這就意味著程序會(huì)進(jìn)行兩次計(jì)算。因此我們可以先用一個(gè)變量存儲(chǔ)s,寫成如下的形式:
Extract[s=Range[1,100],Position[PrimeQ/@s,True]]
避免了二次計(jì)算。值得注意的是,與Haskell這樣的函數(shù)式編程語(yǔ)言所使用的Lazy evaluation不同的是,在計(jì)算以上的語(yǔ)句時(shí),Mathematica采取的策略是先計(jì)算能算的,因此它先計(jì)算的是s=Range[1,100],再計(jì)算內(nèi)部的Position,所以給s賦值要寫在Position外部。如果我們寫成
Extract[s,Position[PrimeQ/@(s=Range[1,100]),True]]
這樣是會(huì)出錯(cuò)的,因?yàn)閟先于Position函數(shù)計(jì)算,此時(shí)它不知道s是什么。
與Map同系列的函數(shù)還有Mapthread、MapIndexed、MapAt、MapAll。就經(jīng)驗(yàn)來(lái)看,Mapthread和MapIndexed使用得較其它兩個(gè)更多。
MapIndexed顧名思義就是帶目錄的映射,Map[f,{a,b}]的作用是生成{f[a],f[b]},而MapIndexed[f,{a,b}]就是生成{f[a,{1}],f[b,{2}]}。不過(guò)值得注意的是在傳遞參數(shù)時(shí),目錄是作為一個(gè)單元素表{1}、{2}這樣的形式進(jìn)行傳遞的,因此在原函數(shù)中就要對(duì)第二個(gè)參數(shù)用First[]函數(shù)提取。
MapThread是用于帶多個(gè)參數(shù)的函數(shù)進(jìn)行映射。比如MapThread[f,{a1,a2,a3...},{b1,b2,b3...}]得到的就是{f[a1,b1],f[a2,b2],f[a3,b3]...}。事實(shí)上這兩個(gè)函數(shù)可以都用Map實(shí)現(xiàn)。MapThread[f,{a1,a2,a3...},{b1,b2,b3...}]與Map[f,{{a1,b1},{a2,b2},{a3,b3}....}]的效果是差不多的。而MapAt可以指定將函數(shù)映射在某個(gè)或多個(gè)元素的位置上。MapAll可以將函數(shù)映射到表中的每個(gè)元素的每個(gè)子表達(dá)式上。MatAll[f,{a,{b}}]就可以實(shí)現(xiàn){f[a],f[f[b]]}。,其中的元素{b}有兩個(gè)層次 ,一是外部的表,二是內(nèi)部的元素,MapAll就映射兩次。