47 写好代码
词汇表
| FromDigits[list] | 从数字列表组装整数 |
| IntegerReverse[n] | 反转整数的数字 |
| Timing[expr] | 计算并计时所用时间 |
| Iconize[expr] | 将表达式以图标形式显示 |
写好代码在许多方面就像写好散文:需要把想法弄清楚,并能清晰地表达。 刚开始写代码时,你大多会先用英语或其它自然语言来思考代码做什么。 但当你对 Wolfram 语言越熟练,你会开始直接用代码思考,输入程序往往比描述它更快。
Wolfram 语言的设计目标是尽可能让它表达事物变得容易。 Wolfram 语言中的函数类似于自然语言中的词汇,设计者也尽力为它们选择恰当的名字。
像 Table、NestList 或 FoldList 这样的函数存在于 Wolfram 语言中,是因为它们表达了人们常想做的事情。 和自然语言一样,总有许多方式可以表达同一件事。 但写好代码意味着找到最直接、最简单的表达方法。
平方数示例
良好代码
要创建前 10 个平方数的表,有一个显然的好代码,它直接使用 Table 函数。
简单且良好的 Wolfram 语言代码,用于生成前 10 个平方数的表:
In[]:=Table[n^2, {n, 10}]
Out[]={1, 4, 9, 16, 25, 36, 49, 64, 81, 100}
糟糕代码
为什么有人会写别的方式? 一个常见问题是没有考虑“整个表”,而是只想到构建表的步骤。 在计算机早期,机器能力有限,别无选择,只能给出描述每一步的代码。
一个更糟糕的分步构建表的代码示例:
In[]:=Module[{list, i}, list = {};
For[i = 1, i <= 10, i++, list = Append[list, i^2]]; list]
Out[]={1, 4, 9, 16, 25, 36, 49, 64, 81, 100}
但 Wolfram 语言的要点是让人以更高层次表达,并创建尽可能直接捕捉意图的代码。 了解语言之后,以这种层次编程会更高效,也更容易为计算机和人理解。
代码迭代
写好代码时,常常要问:“这段代码想做的总体是什么?” 你可能一开始只理解某部分,写出只针对该部分的代码,然后不断扩展。 但若考虑总体,你可能突然意识到有更强大的函数(比如 Fold)可以让代码再次简洁。
构建整数
制作将 {百位, 十位, 个位} 数字转为单个整数的代码:
In[]:=fromdigits[{h_, t_, o_}] := 100 h + 10 t + o
运行代码:
In[]:=fromdigits[{5, 6, 1}]
Out[]=561
推广
将其推广到任意长度的列表,使用 Table:
In[]:=fromdigits[list_List] :=
Total[Table[10^(Length[list] – i)*list[[i]], {i, Length[list]}]]
新代码工作正常:
In[]:=fromdigits[{5, 6, 1, 7, 8}]
Out[]=56178
简化
通过同时乘以整列 10 的幂来简化代码:
In[]:=fromdigits[list_List] :=
Total[10^Reverse[Range[Length[list]] – 1]*list]
尝试新方法
尝试不同的递归方法,先清除先前定义:
In[8]:=Clear[fromdigits]
In[9]:=fromdigits[{k_}] := k
In[10]:=fromdigits[{digits___, k_}] := 10*fromdigits[{digits}] + k
新方法也可行:
In[]:=fromdigits[{5, 6, 1, 7, 8}]
Out[]=56178
洞察过程本质
但随后你会意识到:其实这完全就是一个 Fold!
In[]:=Clear[fromdigits]
In[]:=fromdigits[list_] := Fold[10*#1 + #2 &, list]
In[]:=fromdigits[{5, 6, 1, 7, 8}]
Out[]=56178
使用内置函数
当然,也有内置函数可以完成相同任务:
In[]:=FromDigits[{5, 6, 1, 7, 8}]
Out[]=56178
代码简洁优点
为什么代码简洁好? 首先,因为更可能是正确的。 复杂代码更容易藏错。 简单代码通常更通用,能覆盖未预见的情况,减少额外代码。 最后,简单代码通常更易读和理解
短未必好
但“更短”未必更好,短小的“代码诗”有时难以理解。
一个过短但开始难以理解的 fromdigits 实现:
In[]:=fromdigits = Fold[{10, 1} . {##} &, #] & ;
它仍然可用:
In[]:=fromdigits[{5, 6, 1, 7, 8}]
Out[]=56178
拆分复杂代码
如果要做的事情本身复杂,代码可能不可避免地复杂。 良好代码应拆分为尽可能简单且自包含的函数和定义。 即便在很大的 Wolfram 语言程序中,单个定义往往也不会超过几行。
下面是一个结合了多个情况的单一定义:
In[]:=fib[n_] :=
If[! IntegerQ[n] || n < 1, "Error",
If[n == 1 || n == 2, 1, fib[n – 1] + fib[n – 2]]]
将其拆分为更简单的多个定义更好:
In[]:=fib[1] = fib[2] = 1;
In[]:=fib[n_Integer] := fib[n – 1] + fib[n – 2]
良好命名
为函数选择好名字是写好代码的重要方面。 对于 Wolfram 语言的内置函数,作者在几十年间做了大量工作去挑选恰当的名字,使短名字能传达函数的本质和使用方式。
当你写代码时,通常会为了某个特定上下文先定义一个新函数。 但通常值得给它一个在其他上下文同样能被理解的名字。 如果找不到好名字,通常说明这个函数不太适合定义。
一个好函数名的标志是:当在代码中看到它时,你能立即知道代码在做什么。 事实上,Wolfram 语言的一个重要特点是,优秀的代码通常比任何文字描述更易读和理解。
如何用白话来描述这点?
In[]:=Graphics[{White,
Riffle[NestList[Scale[Rotate[#, 0.1], 0.9] &,
Rectangle[], 40], {Pink, Yellow}]}]

抉择
当你编写 Wolfram 语言代码时,可能需要在使用单个稀有内置函数(恰好做你想要的事)与用几个常见函数组合实现相同功能之间做选择。 在本书中,我有时选择避免稀有函数以减少词汇量。 但最佳代码往往在可能时使用单个函数——因为函数名能解释代码意图,是单个片段无法替代的。
函数组合
用一小段代码反转整数的数字:
In[]:=FromDigits[Reverse[IntegerDigits[123456]]]
Out[]=654321
单一函数
使用单个内置函数能更清楚地表达意图:
In[]:=IntegerReverse[123456]
Out[]=654321
效率
好代码需要正确且易懂。 但也需要高效运行。 在 Wolfram 语言中,更简单的代码通常也更好,因为清楚的意图让语言更容易优化内部计算方式。
每个新版本中,Wolfram 语言在自动优化执行速度方面都有进步。 但你仍可通过良好构造算法来帮助它。
计时
Timing 给出计算的时间(秒)及其结果:
In[]:=Timing[fib[25]]
Out[]={0.0625, 75025}
可视化比较
画出根据之前定义计算 fib[n] 的时间。 使用上述 fib 定义时,时间增长很快:
In[]:=ListLinePlot[Table[First[Timing[fib[n]]], {n, 25}]]

算法优化
我们使用的算法在重算许多已计算值方面做了指数级的多余工作。 可以通过使 fib[n_] 的定义总是为 fib[n] 赋值,从而存储每个中间计算结果来避免这点。
将 fib 重新定义为记住它计算过的每个值:
In[]:=fib[1] = fib[2] = 1;
In[]:=fib[n_Integer] := fib[n] = fib[n – 1] + fib[n – 2]
现在即使到 1000,每个新值也只需微秒级时间计算:
In[]:=ListLinePlot[Table[First[Timing[fib[n]]], {n, 1000}]]

代码结构
好代码应有易读的结构。 但有时代码中需要包含一些细节,这些细节会妨碍看到主要要点。
图标化
在 Wolfram 笔记本中,有一个方便的方法:图标化这些细节(iconize)。
生成带有大量选项的绘图:
In[]:=ListLinePlot[Table[Prime[n]/n,{n,100}],Frame->True, PlotStyle->Orange,Filling->Axis,
FillingStyle->LightPurple,AspectRatio->1/3,Mesh->All]

将选项图标化后的相同代码:

在典型的 Wolfram 笔记本界面中,你可以选择想图标化的部分,使用右键菜单。 你会看到图标化形式,但底层仍是实际使用的代码。
当你想在笔记本中包含大量数据但不想显式显示时,图标化也很方便。
创建一千个素数的表,但仅以图标化形式显示:
In[]:=Iconize[Table[Prime[n], {n, 1000}]]

你可以复制图标化形式,然后在输入中使用它:

Out[]:=3682913
结语
写好 Wolfram 代码的要点总结:
- 用Wolfram 语言直接思考,迭代优化代码;
- 代码如散文,应优雅易理解;
- 选择恰当的函数,优先使用单一内置函数,使用有意义的命名;
- 拆分复杂逻辑,追求代码结构简洁与清晰;
- 考虑算法效率。
下一步
调试代码



