拜读

没错,就是一遍膜拜一遍读

语录

学术圈三宗罪:思想的监狱、论文的游戏、政治的泛滥。

TODO

其中极少数值得一提的“模式”,也许是 visitor 和 interpreter。很可惜的是,只有很少的人明白如何使用它们。所谓的 visitor,其实本质上就是函数式语言里的含有“模式匹配”(pattern matching)的递归函数。在函数式语言里,这是多么轻松的事情。可是因为 Java 没有模式匹配,所以很多人使用 visitor pattern。为了所谓的“通用性”,他们往往把 visitor pattern 搞出多层继承关系,让你转几道弯也搞不清楚到底哪个 visitor 才是干实事的。

其实,函数式语言的研究者们早就知道 visitor pattern 是怎么得来的。如果你想知道如何从无到有,“发明”出 Java 的 visitor pattern,可以参考 Dan Friedman 跟他的学生 Matthias Felleisen 合写的的另一本“小人书”《A Little Java, A Few Patterns》(发表于 1997 年)。


询问&注意事项

第三方


搜学

《The Little Schemer》(前身叫《The Little Lisper》

TeXmacs

想要真正理解 Java 设计模式的人可以看看这本:A Little Java, A Few Patterns:

braid on steam

。所以我觉得它是学习程序设计最好的入手点和进阶工具。当然 Scheme 也有少数的问题,而且缺少一些我想要的功能,但这些都瑕不掩瑜。在用了很多其它的语言之后,我觉得 Scheme 真的是非常优美的语言。

SICP里貌似有一节就是教你写个符号微分程序。做微积分这种无聊的事情,就是应该交给电脑去做。总之,这从一方面显示了,Lisp 的语法其实超越了传统的数学。

Chez Scheme 生成的“目标代码”效率之高,我还没有见到任何其它 Scheme 编译器可以与之匹敌。而它的“编译速度”之快,没有任何语言的任何编译器可以相提并论(注意我去掉了“Scheme”这个限定词)。Chez Scheme 可以在 5 秒钟之内完成从头到尾的自我编译。想想编译 GCC 或者 GHC 需要多少时间,你就明白差距了。

Kent 的课程编译器

如果你需要一个 Scheme 版本用于学习的话,Chez Scheme 有一个免费的版本叫做 Petite Chez Scheme,可以免费下载。因为 Petite 的出错信息非常不友好,所以我也推荐 Racket 作为替补。不过你需要注意的是,Racket 的速度比起 Chez Scheme 是天壤之别。

专门讲语义的书很少,现在推荐一本我觉得深入浅出的:《Programming Languages and Lambda Calculi》。只需要看完前半部分(Part I 和 II,100来页)就可以了。这书好在什么地方呢?它是从非常简单的布尔表达式(而不是 lambda calculus)开始讲解什么是递归定义,什么是解释,什么是 Church-Rosser,什么是上下文 (evaluation context)。在让你理解了这种简单语言的语义,有了足够的信心之后,才告诉你更多的东西。比如 lambda calculus 和 CEK,SECD 等抽象机 (abstract machine)。理解了这些概念之后,你就会发现所有的程序语言都可以比较容易的理解了。


能做的事越多,代码量却越少。也许这就叫做程序的“美”,它跟数学的“美”其实是一回事。

美的程序不可能从修修补补中来。它必须完美的把握住事物的本质,否则就会有许许多多无法修补的特例。其实程序员跟画家差不多,画家如果一天到头蹲在家里,肯定什么好东西也画不出来。程序员也一样,蹲在家里面对电脑,其实很难写出什么好的代码。你必须出去观察事物,寻找“灵感”,而不只是写代码。在修改代码的时候,你必须用“心灵之眼”看见代码背后所表达的事物。这也是为什么很多高明的程序员不怎么用调试器(debugger)的原因。他们只是用眼睛看着代码,然后闭上眼,脑海里浮现出其中信息的流动,所以他们经常一动手就能改到正确的地方。


Chez Scheme

世界上最快,最成熟可靠的 Scheme 实现是 R. Kent Dybvig 所作的 Chez Scheme。它可以把 Scheme 编译成机器代码,运行速度非常高。Chez Scheme 曾经是商业软件,价格昂贵,然而现在却开源了,并且可以免费使用。你可以在这里下载 Chez Scheme 的源代码:

https://github.com/cisco/ChezScheme

编译安装很快很方便,在 Linux 和 Mac 系统基本就是这样:

1
2
3
./configure
make
sudo make install

为了让段落的行看起来均匀,我使用了一种类似 TeX 的动态规划断行算法。它先算出多种断行方案的“难看程度”,然后从中选出最好看的一个。


。Lisp Machine 似乎是其中最接近的一个。Oberon 是另外一个。IBM System/38 是类似系统里面最老的一个。最近一些年出现的还有微软的 Singularity,另外还有人试图把 JVM 和 Erlang VM 直接放到硬件上执行。


结构化编辑器


怎么说呢,我觉得每个程序员的生命中都至少应该有几个月在静心学习 Haskell。学会 Haskell 就像吃几天素食一样。每天吃素食显然会缺乏全面的营养,但是每天都吃荤的话,你恐怕就永远意识不到身体里的毒素有多严重。


first-class function


没有任何一种语言值得你用毕生的精力去“精通”它。“精通”其实代表着“脑残”——你成为了一个高效的机器,而不是一个有自己头脑的人。你必须对每种语言都带有一定的怀疑态度,而不是完全的拥抱它。


当莫扎特开始写乐谱时,作品就已经完成了。他的手稿一气呵成,书法也很好。贝多芬不一样,他总是在怀疑和挣扎。他的作品一般是还没有想好就开始写,然后就往上面贴纸条修改。有一次贝多芬改了9遍才把手稿完成,后来有人把这手稿一层层的撕开,发现第一版和最后一版是一摸一样的。这种改来改去的做法是 Anglo-Saxon 民族的传统,它贯穿了英国式的教育。

先设计好再写代码


如果你的程序真的优雅,那么它就会容易管理。第一是因为它比其它的方案都要短,第二是因为它的组件都可以被换成另外的方案而不会影响其它的部分。很奇怪的是,最优雅的程序往往也是最高效的。


goroutine

Goroutine 可以说是 Go 的最重要的特色。很多人使用 Go 就是听说 goroutine 能支持所谓的“大并发”。

首先这种大并发并不是什么新鲜东西。每个理解程序语言理论的人都知道 goroutine 其实就是一些用户级的 “continuation”。系统级的 continuation 通常被叫做“进程”或者“线程”。Continuation 是函数式语言专家们再了解不过的东西了,比如我的前导师 Amr Sabry 就是关于 continuation 的顶级专家之一。

Node.js 那种 “callback hell”,其实就是函数式语言里面常用的一种手法,叫做 continuation passing style (CPS)。由于 Scheme 有 call/cc,所以从理论上讲,它可以不通过 CPS 样式的代码而实现大并发。所以函数式语言只要支持 continuation,就会很容易的实现大并发,也许还会更高效,更好用一些。比如 Scheme 的一个实现 Gambit-C 就可以被用来实现大并发的东西。Chez Scheme 也许也可以,不过还有待确认。

当然具体实现上的效率也许有区别,然而我只是说,goroutine 其实并不是像很多人想象的那样全新的,革命性的,独一无二的东西。只要有足够的动力,其它语言都能添加这个东西。


在每一家公司里,我都是那个支持“80% 计划”的人。也就是说,用 20% 的时间和精力完成 80% 的目标,尽快的试错,迅速返回,修正方向,如此反复…… 这跟许多技术管理者的方式是不一样的。我亲眼看见许多的管理者成天叫嚣着“Agile”,却耗费几个月甚至几年的时间,来做一件我一眼就知道会失败的事情,白白耗费公司的资源。


一个模块应该像一个电路芯片,它有定义良好的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以你其实根本不需要把代码分开在多个文件或者目录里面,同样可以完成代码的模块化。

避免写太长的函数。如果发现函数太大了,就应该把它拆分成几个更小的。通常我写的函数长度都不超过40行。

制造小的工具函数。如果你仔细观察代码,就会发现其实里面有很多的重复。这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。有些帮助函数也许就只有两行,然而它们却能大大简化主要函数里面的逻辑。

因为他们想避免函数调用的开销,结果他们写出几百行之大的函数。这是一种过时的观念。现代的编译器都能自动的把小的函数内联(inline)到调用它的地方,所以根本不产生函数调用,也就不会产生任何多余的开销。 他们使用宏,其实是为了达到内联的目的。然而能否内联,其实并不是宏与函数的根本区别。宏与函数有着巨大的区别(这个我以后再讲),应该尽量避免使用宏。为了内联而使用宏,其实是滥用了宏,这会引起各种各样的麻烦,比如使程序难以理解,难以调试,容易出错等等。


每个函数只做一件简单的事情。有些人喜欢制造一些“通用”的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来“选择”这个函数所要做的事情。如果你发现两件事情大部分内容相同,只有少数不同,多半时候你可以把相同的部分提取出去,做成一个辅助函数。


重视语言特性

举一些语言特性的例子:

  • 变量定义
  • 算术运算
  • for 循环语句,while 循环语句
  • 函数定义,函数调用
  • 递归
  • 静态类型系统
  • 类型推导
  • lambda 函数
  • 面向对象
  • 垃圾回收
  • 指针算术
  • goto 语句

一个高明的程序员如果开始用一种新的程序语言,他往往不是去看这个语言的大部头手册或者书籍,而是先有一个需要解决的问题。手头有了问题,他可以用两分钟浏览一下这语言的手册,看看这语言大概长什么样。然后,他直接拿起一段例子代码来开始修改捣鼓,想法把这代码改成自己正想解决的问题。在这个简短的过程中,他很快的掌握了这个语言,并用它表达出心里的想法。

在这个过程中,随着需求的出现,他可能会问这样的问题:

  • 这个语言的“变量定义”是什么语法,需要“声明类型”吗,还是可以用“类型推导”?
  • 它的“类型”是什么语法?是否支持“泛型”?泛型的 “variance” 如何表达?
  • 这个语言的“函数”是什么语法,“函数调用”是什么语法,可否使用“缺省参数”?
  • ……

他是带着问题找特性,就像查字典一样,而不是被淹没于大部头的手册里面,昏昏欲睡一个月才开始写代码。


如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个class,而且更加容易理解,不易出错


使用有意义的函数和变量名字。如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。

局部变量应该尽量接近使用它的地方。

局部变量名字应该简短。successInDeleteFile这种“camelCase”,如果超过了三个单词连在一起,其实是很碍眼的东西。所以如果你能用一个单词表示同样的意义,那当然更好。

不要重用局部变量。

把复杂的逻辑提取出去,做成“帮助函数”。

把复杂的表达式提取出去,做成中间变量……这样的代码一行太长,而且嵌套太多,不容易看清楚。其实训练有素的函数式程序员,都知道中间变量的好处,不会盲目的使用嵌套的函数……这样写,不但有效地控制了单行代码的长度,而且由于引入的中间变量具有“意义”,步骤清晰,变得很容易理解。

在合理的地方换行。

避免使用自增减表达式(i++,++i,i–,–i)有人也许以为i++或者++i的效率比拆开之后要高,这只是一种错觉。这些代码经过基本的编译器优化之后,生成的机器代码是完全没有区别的。自增减表达式只有在两种情况下才可以安全的使用。一种是在for循环的update部分,比如for(int i = 0; i < 5; i++)。另一种情况是写成单独的一行,比如i++;。这两种情况是完全没有歧义的。你需要避免其它的情况,比如用在复杂的表达式里面,比如foo(i++)foo(++i) + foo(i),…… 没有人应该知道,或者去追究这些是什么意思。

永远不要省略花括号。

合理使用括号,不要盲目依赖操作符优先级。(减轻记忆运算符优先级的负担)

避免使用continue和break。循环语句(for,while)里面出现return是没问题的,然而如果你使用了continue或者break,就会让循环的逻辑和终止条件变得复杂,难以确保正确。

  1. 如果出现了continue,你往往只需要把continue的条件反向,就可以消除continue。
  2. 如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break。
  3. 有时候你可以把break替换成return,从而去掉break。
  4. 如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后continue或者break就可以去掉了。

如果有更加直接,更加清晰的写法,就选择它,即使它看起来更长,更笨,也一样选择它。

无懈可击:我提到了自己写的代码里面很少出现只有一个分支的if语句。我写出的if语句,大部分都有两个分支

Java的函数如果出现问题,一般通过异常(exception)来表示。你可以把异常加上函数本来的返回值,看成是一个“union类型”。比如:

1
2
3
String foo() throws MyException {
  ...
}

如果你把异常catch了,忽略掉,那么你就不知道foo其实失败了。这就像开车时看到路口写着“前方施工,道路关闭”,还继续往前开。这当然迟早会出问题,因为你根本不知道自己在干什么。

catch异常的时候,你不应该使用Exception这么宽泛的类型。你应该正好catch可能发生的那种异常A。使用宽泛的异常类型有很大的问题,因为它会不经意的catch住另外的异常(比如B)。你的代码逻辑是基于判断A是否出现,可你却catch所有的异常(Exception类),所以当其它的异常B出现的时候,你的代码就会出现莫名其妙的问题,因为你以为A出现了,而其实它没有。这种bug,有时候甚至使用debugger都难以发现。

如果你在自己函数的类型加上throws Exception,那么你就不可避免的需要在调用它的地方处理这个异常,如果调用它的函数也写着throws Exception,这毛病就传得更远。我的经验是,尽量在异常出现的当时就作出处理。否则如果你把它返回给你的调用者,它也许根本不知道该怎么办了

另外,try { … } catch里面,应该包含尽量少的代码。比如,如果foo和bar都可能产生异常A,你的代码应该尽可能写成:

try { foo(); } catch (A e) {…}

try { bar(); } catch (A e) {…}

函数作者:明确声明不接受null参数,当参数是null时立即崩溃。不要试图对null进行“容错”,不要让程序继续往下执行。如果调用者使用了null作为参数,那么调用者(而不是函数作者)应该对程序的崩溃负全责。

采用强硬态度一个很简单的做法是使用Objects.requireNonNull()。它的定义很简单:

public static T requireNonNull(T obj) { if (obj == null) { throw new NullPointerException(); } else { return obj; } } 你可以用这个函数来检查不想接受null的每一个参数,只要传进来的参数是null,就会立即触发NullPointerException崩溃掉,这样你就可以有效地防止null指针不知不觉传递到其它地方去。

使用@NotNull和@Nullable标记。IntelliJ……

过度工程即将出现的一个重要信号,就是当你过度的思考“将来”,考虑一些还没有发生的事情,还没有出现的需求。实际上没做多少事情,却为了所谓的“将来”,加入了很多不必要的复杂性。眼前的问题还没解决呢,就被“将来”给拖垮了。人们都不喜欢目光短浅的人,然而在现实的工程中,有时候你就是得看近一点,把手头的问题先搞定了,再谈以后扩展的问题。另外一种过度工程的来源,是过度的关心“代码重用”。过度地关心“测试”,也会引起过度工程。有些人为了测试,把本来很简单的代码改成“方便测试”的形式,结果引入很多复杂性,以至于本来一下就能写对的代码,最后复杂不堪,出现很多bug。

先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。 先写出可用,简单,明显没有bug的代码,再考虑测试的问题。


求职者,在面试的时候最好深刻了解他们的性格,态度和做事方式,看他们是否能看淡这些,能否平等对待其他人,能否理性而实在的对待工程。否则自视很高的“编译器人”进了公司,很可能对团队成为一种灾难。


毕设装逼用

制造自然语言的 parser 有多难?很多人可能没有试过。我做过这事。在 Indiana 的时候,我为了凑足学分,修了一门 NLP 课程,跟几个同学一起实现了一个英语语法的 parser。它分析出来的语法树形式,就像上面的那样。

你可能想不到有多困难,你不仅要深刻理解编程语言的 parser 理论(LL,LR,GLR……),还得依靠大量的例子和数据,才能解开人类语言里的各种歧义。我的合作伙伴是专门研究 NLP 的,把什么 Haskell,类型系统,category theory,什么 GLR parsing 之类…… 都弄得很溜。然而就算如此,我们的英语 parser 也只能处理最简单的句子,还错误百出,最后蒙混过关 :P