如何用最簡單的代碼說明Mathematica裡面的各個水平層次?

釣魚~

感覺這個問題還挺有趣的,因為感覺我自己不同時間拿mma寫的代碼風格都完全不同的,就來知乎裡面看看大家有啥好玩的短代碼~


挺有意思的問題,特地回去翻了下電腦里的所有nb文件,試著總結下我學mma以來的代碼風格變化歷程吧,「說明各個水平層次」遠遠夠不上,僅供娛樂~

(多圖預警)

  • 高級計算器階段

最開始接觸MMA完全就是當成能解方程、算積分、畫圖的高級計算器來使,所有代碼都是一個函數。後來學會了給函數加選項,但本質上還是計算器┑( ̄Д  ̄)┍

這個階段的代碼最大特點是經常手動:需要之前的計算結果?手動把之前的輸出複製粘貼到下面的代碼里!需要生成個列表?手動敲!所以導致這個階段的代碼都長且亂(嚴格來講我並不想承認這種東西是代碼。。。)

附黑歷史代碼一段,普物試驗處理數據用的,寫於14年3月。感受一下這靈壓吧

加強版C階段

後來學了C,然後發現MMA里也有For、While、If等C語言常用函數,然後就開始拿MMA當成帶了好多高科技功能的C來寫,這一階段持續了很長一段時間。。。

這一階段代碼的主要特點是大量使用For、AppendTo等在MMA中十分低效的函數,以及使用DownValue而非列表來作為數組等等。導致代碼長度感人,效率堪憂。一言以蔽之,長得像C跑得賊慢。

下面是一段用於估測楊氏模量的程序,寫於14年7月底。雖然現在我非常討厭這種C風格的MMA代碼,但必須承認這種代碼易讀性還是很強的。。。而且就算不熟悉MMA的語法也能比較容易地看懂

  • MMA入門階段

再後來慢慢學會了純函數和模式,開始使用Map、Apply、ReplaceAll等比較MMA風格的函數(以及符號簡寫)。另外開始老老實實的用List+Part當做數組,a[i]這種形式大多數情況下只作為函數使用。

這一階段代碼的簡潔性和效率都有了很大的提升,而且看上去非常的「MMA」。但作為代價的是可讀性大大降低了,許多對MMA語法不熟悉的人(包括當年的我)看到一堆#~@之類奇怪的符號真是頭大如斗,完全失去繼續閱讀的信心。

(補充一下,這裡的「可讀性降低」主要是對MMA語法不太熟悉的人而言,熟悉了以後反而會覺得這種寫法更親切。比如我現在覺得後綴表達式比正常的寫法讀起來舒服得多)

下面這段代碼是演示不動點迭代的,寫於15年4月。因為比較短所以把代碼也發上來了,可以運行一下試試,還是挺好玩的~

Manipulate[
Plot[{a x (1 - x), x}, {x, 0, 1},
Epilog -&>
Line[{#[[All, {1, 1}]], #}[Transpose]~Flatten~1 @
Partition[NestList[a # (1 - #) , x0, 40], 2, 1]],
AspectRatio -&> Automatic], {a, 1, 4}, {{x0, 0.6}, 0, 1}]

  • 奇技淫巧階段

先空著明天補

  • 走火入魔階段

終於寫到這了!其實我最開始答題就是想貼下面這張圖片,求n位水仙花數的程序。

上個月在BBS看到個求水仙花數的題,隨手寫了個函數,寫完之後才想起來去年寫過一個。兩個函數放在一起對比,我陷入了深深的思考:這一年在我身上都TM發生了什麼……

附上代碼供各位嘗試解讀。你們儘管看,看懂了算我輸(● ̄(?) ̄●)

Narcissistic[n_] :=
Reap[Scan[Module[{count = Join @@ ConstantArray @@@ #},
Scan[
If[(Sort@Tally@IntegerDigits[#^n .count] ==
Sort@ Thread[{#, count}]), Sow[#^n .count]] ,
Fold[Function[{digitlist, num},
Flatten[Function[e,
Join[e, #] /@
Subsets[Complement[Range[0, 9], e], {num}]] /@
digitlist, 1]], {{}}, #[[All, 2]]]]
] , Sort /@ Tally /@ IntegerPartitions[n, n]]][[2, 1]];
Narcissistic[10] // AbsoluteTiming


看到 @無影東瓜 提到n位水仙花數的例子,以前也寫過這個,寫過好幾次

C語言風格,速度很慢

熟悉更多內置函數和語言特性後,使用列表操作,代碼變得簡潔了,速度也快了一些

Select的速度一般不是很快,可以用Compile加速;不過使用下面的寫法會自動Compile,以及用Pick代替Select,比上面的速度更快

用Outer構造列表,又快一些

如果想計算10位以上的水仙花數,上面的方法就不行了,時間複雜度太高,要考慮優化一下演算法,不再逐個數進行枚舉,而是枚舉組合

生成組合在Python中用itertools.combinations_with_replacement很方便,Mathematica中沒有等價的函數,可以通過IntegerPartitions或Subsets間接構造,這樣計算19位以內的需要1分鐘多

還是上面的演算法,主要是用了向量化操作+並行,在多核PC上提速會比較明顯(測試電腦為4核,把ParallelTable改成Table後用時31秒)

元編程生成代碼——19層Do循環+Compile,這種寫法主要為了好玩。其實速度還真不慢,比得上上面並行的版本了

比如narc[9]生成的代碼為


先貼一段我現在的代碼風格吧,感覺處在下面提到的第三和第四階段之間。水平不高,輕噴。

第一個階段:用一些碎片化的功能來代替手算。比如求導,求級數展開,求積分,求微分方程的解,求矩陣特徵值,求線性方程組……這時候只需要寫一行代碼即可。

In[1]:= 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9

Out[1]= 45

第二個階段:不懂內部機制,不熟悉常用函數,不懂設計哲學,甚至還不太熟悉怎麼看它的手冊。這時候用循環和條件寫一點小東西沒問題,但是它就是一個極慢的C語言,談不上多麼實用。

In[2]:= For[i = 1; j = 0, i &< 10, i++, j = j + i]

In[3]:= j

Out[3]= 45

第三階段:開始試著了解它的數據類型,函數式編程,模式替換,純函數,各種語法糖。開始學會看它的幫助文檔,也初步的熟悉了一些比較常用的函數。這時候就可以寫一些能用的小規模程序了,但是有時候程序很醜陋,經常傻傻的把已經存在的自帶函數重寫一遍。此外這個時候就漸漸有能力使用別人寫好的程序包了。

In[4]:= Plus @@ Table[i, {i, 9}]

Out[4]= 45

第四階段:積累了越來越多好用的函數,代碼越來越優雅。根據不同使用目的開始關注特殊函數、畫圖、GUI、多線程、math腳本、編譯、程序介面等更加特異性的內容。閑著無聊的時候也會比較不同代碼實現速度的差異。寫程序的時候會讓程序變得更加模塊化、標準化,慢慢的就可以把各種小的模塊拼裝起來,讓程序規模越來越大。

In[5]:= sum[x_] := Module[{x0 = x}, Plus @@ Range[x0]];

In[6]:= sum[9]

Out[6]= 45

再說點題外話,mathematica這個語言的前身是一個符號運算語言,整體的構架也是面向函數的,如果一個人的代碼里出現For,While這樣的面向過程語句,那麼他很可能還是把這個語言當成一個「擁有很多好用功能的計算器」。用Range(或者Table、List)代替For,用模式替換代替While,在很多場合下會具有更好的性能和更強大的功能。


用Mathematica兩年了,有一點點編程的底子,平常用Mathematica寫寫作業做做research之類的。不敢說我下面的code能反映Mathematica使用者的水平高低——有倆code都是我教授寫的…唯一肯定的就是無論數學還是編程水平都是我最低orz——但也許反映了些相關專業背景和Mathematica的版本對編碼風格所帶來的影響吧,很多用別的語言寫略複雜的程序,Mathematica很多都可以一行搞定。

這幾段code都是關於數值分析里的一個經典的求根法Newton"s method (Newton#x27;s method - Wikipedia)。

第一段是我一個領域是numerical analysis 和 combinatorics 的華裔數學教授大約十多年前寫的, 他是80年代大陸某理工院校博士畢業和美國某top50大U博士畢業…數值分析應該算他本行了,一直到2000s在這個領域都還挺活躍的。寫個各種碼完全對他來說輕車熟路,教授自學的編程…C++ java什麼的,不知道這段是什麼風格的,之前一直借來運行一些作業就留在我電腦里了…

然後下面這段是我TA的一個教授幾年前給學生講課的時候寫的,他是做combinatorics的,用Mathematica也有一段時間了,這段碼寫的其實一般,主要是給學生演示newton"s method 的幾何解釋用的,雖然有點難看吧…但加了一個manipulate還是蠻有趣…

第三段碼是去年我寫的,我是上了大學數學課的TA教的我Mathematica,但真正自己用起來還是在自己做research的時候,本來一直用第一個教授的碼跑newton"s method求根…但後來當了TA之後,要教課還是要自己寫一個,然後就用了NestWhileList寫了一個簡單的,不用手動swapping了,直接用#代替函數里的值,話說還是蠻簡陋的都沒封裝起來…Mathematica里越來越多的built-in function讓在Mathematica里編程越來越簡潔了,雖然還是很多語句在Mathematica里實現不了,但簡單計算些大體的東西用Mathematica寫還是方便很多。


用最簡單的代碼——階乘,來看一看 Mathematica 的代碼風格多樣性。

圖片出處:中科院軟體中心《Mathematica 和 Wolfram|Alpha 在教育和研究中的應用》

百度雲鏈接: http://pan.baidu.com/s/1qY10SCs 密碼: ru75 (已許可)我自己寫了一個計時器比拼了一下各種寫法的效率,如下圖(右邊第一欄是10!的結果,第二欄是 RepeatedTiming 的耗時):

從運行效率和代碼簡潔程度來說,Mathematical &> Rule-based &> Procedural &> Recursive &> Functional &> Pattern-based &> String-based &> Constructive

當然不同的實現方式在解決不同問題時也各有優勢,另外這個展示的代碼中有些明顯寫就要比其他複雜一些,所以具體情況具體分析,僅供參考。個人而言比較喜歡用 Rule-based 和 Funtional 的方式,當然如果你寫大的項目想用到面向對象的方法來寫也不是不可以,這裡推薦一篇文檔:MathematicaForPrediction/Implementation-of-Object_Oriented-Programming-Design-Patterns-in-Mathematica.md at master · antononcube/MathematicaForPrediction · GitHub,Mathematica 中面向對象的畫風是這樣的:

Clear[AbstractClass, ConcreteOne, ConcreteTwo];

CLASSHEAD = AbstractClass;
AbstractClass[d_]["Data"[]] := d;
AbstractClass[d_]["PrimitiveOperation1"[]] := d[[1]];
AbstractClass[d_]["PrimitiveOperation2"[]] := d[[2]];
AbstractClass[d_]["TemplateMethod"[]] := CLASSHEAD[d]["PrimitiveOperation1"[]] + CLASSHEAD[d]["PrimitiveOperation2"[]]

ConcreteOne[d_][s_] := Block[{CLASSHEAD = ConcreteOne}, AbstractClass[d][s]]
ConcreteOne[d_]["PrimitiveOperation1"[]] := d[[1]];
ConcreteOne[d_]["PrimitiveOperation2"[]] := d[[1]]*d[[2]];

ConcreteTwo[d_][s_] := Block[{CLASSHEAD = ConcreteTwo}, AbstractClass[d][s]]
ConcreteTwo[d_]["PrimitiveOperation1"[]] := d[[1]];
ConcreteTwo[d_]["PrimitiveOperation2"[]] := d[[3]]^d[[2]];

引用自MathematicaForPrediction/Implementation-of-Object_Oriented-Programming-Design-Patterns-in-Mathematica.md at master · antononcube/MathematicaForPrediction · GitHub


有趣的代碼自然有很多,但是我原本想問的不是這種長代碼,而是可以對號入座的,各個水平層次的人很明顯的特徵。當然上面回答的也不錯,我這也就當個補充吧,這裡我對老手的定義不是Mr.Wizard或者Lenoid Shrifin那級別的,而是Level4~5的Mathematica用戶,恕我將自己分類到這裡……:

聲明:底下代碼不要糾結對錯啊,什麼具體寫出來有多難啊,一家之言,僅供參考好吧:

核心語言:

小白0:不可描述,各種拷貝Excel數據,滿篇都是數據,代碼找不到……

小白1:For[i=1,i&<=10,i++,AppendTo[list,i]]

小白2:Do[AppendTo[list,i],10]

入門:Table[i,10],當然最好的是Range@10

新手:Table[list[[i-1]]+list[[i]],{i,2,10}]

中等:Most@list+Rest@list

也不算老手吧:ListCorrelate[{1,1},list](當然,更加複雜的問題上這個方法更好)

新手:Append[Table[list[[i]]+list[[i+1]],{i,10}],list[[10]]+list[[1]]]

中等:list+RotateRight@list;

也不算老手吧:ListCorrelate[{1,1},list,{1,1}]

新手:f[x_]:=x^2

中等:f[{x_,y__}]:=f/@{y}

老手:g/:f[g[e:(x_,{y_,z:1})..],Verbatim[x_]]:=Block[{},一大坨/;y^2&<=3](這段代碼瞎寫的,只是表現一下碰到這類的低頭就對了……)

新手/中等:{Dynamic[list[[1]]],Dynamic[list[[2]]],Dynamic[list[[3]]],Dynamic[list[[4]]]}(中等玩家發現沒法用Table啊,也很無奈啊,當然這裡可以用Dynamic[list[[#]]]/@Range@4,不過我為了演示,先不寫了吧……)

老手:我自己寫的一段代碼……用來處理類似的問題的……慚愧慚愧……

cdynamic[namef_, namev_] := Function[{index0}, namef[Dynamic @@ With[{}, Most@index0 /. {seq___} :&>Hold[Which[And @@ Flatten@namev[[seq]],True, ! (Or @@ Flatten@namev[[seq]]), False, True, Null],Function[{tf, expr}, MapIndexed[Function[{val, index},namev[[seq, Sequence @@ index]] = tf],namev[[seq]], {-1}]]]]]]

新手:l=Length@x;Table[f[g[x[[n]]]],{n,Length@x}]

中等:Table[f@g@xx,{xx,x}] OR f/@g/@x

老手:f@*g/@x

雖然我水平不濟,達不到太高的高度,但是依我現在看來,MMA達到我這種級別的所謂「老手」主要難點在於仨,一個是對List和表達式的操作不易熟練(小白到中等,開始對Mathematica有感覺),二是對Mathematica的特點,結構,運算方式了解不夠清楚(中等到接近老手,開始覺得Mathematica是一個獨特的語言並且對其有好感),三是對運算的掌控力不夠(可以說特指Hold,Unevaluated,Defer這類的)(老手到老手以上其實我覺得差就差在這裡了,我反正嘗試了解Mathematica的運算模式啊,整體設計啊之後,現在對Mathematica精妙的設計讚不絕口,Mathematica真是獨一無二的,及其方便而又極度精巧的)

對了,看瓜哥附上了代碼,我也來一小段我覺得我寫的令我自己覺得智商比重最高(高難度寫的好的代碼倒還有,例如這裡 https://pan.baidu.com/s/1clDuuq ,但是這段整體上來說,靈光閃現的密度最大)的一段小代碼吧……

SetAttributes[adddebug, HoldFirst]

adddebug[orig_, func_: Echo, level_: {2}] :=

Defer@@Module[{$count = 1, tempf},

SetAttributes[tempf, HoldAll];

Replace[Replace[Hold[orig], HoldPattern[CompoundExpression[comp__]] :&>

RuleCondition[tempf @@Riffle[List @@ (tempf /@ Hold[comp]),

tempf@func@# /@Range[$count, ($count = $count + Length@Hold[comp] + 1) -

1], {2, -1, 2}]],

level],

tempf[seg__] :&> RuleCondition[tempf[CompoundExpression] @@ {seg}],

level] /. tempf[seg_] :&> seg

]

adddebug[Do[j = i^2; Thread[{{1}, {2, 3}}], {i, 3}]]

這段代碼的功用很簡單,就是在每兩句話中插入一句話,例如Print@1,方便調試而已,但是寫起來很煩啊……算是Meta-Programming吧

對了,看懂了我不算輸,畢竟的確不難看懂……只是覺得裡面有幾步用的很巧妙而已……不過,看懂了還是可以在下面評論倆句的么~

還有好多,想到再加。


看了這個回答,

原來我之前用的一直都是Cathematica…


水平不夠,但是想答,比如看到樓上有求階乘,結合著說說自己成長之路

高級計算器

jiecheng[n_] := n!

懂點編程,但是不熟悉MMA

jiecheng2[n_] := Module[{shu = 1},

For[i = 1, i &<= n, i++,

shu *= i];

shu]

漸入佳境

jiecheng3[1] = 1;

jiecheng3[n_] := jiecheng3[n] = jiecheng3[n - 1]*n;

說漸入佳境是因為jiecheng3[n_] := jiecheng3[n] 比一般遞歸要快,已經開始接觸MMA核心內容了

重頭再來

jiecheng4 = #!

重頭再來是因為知道了函數式編程

二逼時代

Length@Permutations@Range[1, #]

到了天上地下唯我獨尊的時候了

返璞歸真

Factorial[]

如果有內置函數絕對不會自己寫

以上是我個人的成長曆程


推薦閱讀:

為什麼mathematica的語法和lisp很近?
用mathematica寫作業有哪些提高效率的使用技巧?
為何Mathematica解三角函數方程組可行性這麼低呢?
Mathematica 做數值計算時有哪些方式可以達到提速的目的?
如何使用mathematica發送郵件?

TAG:WolframMathematica |