编程的正确性
2020-05-07T21:41:50+08:00 | 10分钟阅读 | 更新于 2024-08-24T18:26:27+08:00
The Worse is Better.
这是一篇我很早就想去写的博客,我想借我这几年的编程经历,来阐述一个事实。当然,这不是我发现的事实,我只是站在了巨人的肩膀上而已。
只是从发现这个事实,到亲身体会,再到能差不多理解大师们所说的这个事实,条件可能太苛刻了。
现在决定来写,也是因为我觉得我差不多能理解了。
这是一篇怀疑正确性的博客,如果你有编程经验,应该能快速地看懂,但是可能不会很快的理解,我会尽量说的比较通俗。没有编程经验的,也不妨看个乐子。
这个事实就是,Worse is Better,更坏的却是更好的。
简单、正确、一致、完整
在语言和软件的设计中,简单性、正确性、一致性、完整性这四个特征,是极其重要的。
其中就有两大流派,分别是 MIT/Stanford Style(以下简称 MIT 流派) 和 New Jersey Style(以下简称新泽西流派,是的,就是贝尔实验室所在的那个州), 这两个派别对这四个特征的所偏向的点是完全不同的,哪里不同呢?
MIT 流派是这样认为的:正确性是最重要的,不允许出现不正确的地方。一致性也很重要,你所设计的东西不能出现不一致的情况,一致性和正确性基本同等重要。 完整性排在这两个后面,你要说你设计的东西不完整,不能涵盖所有的情况,那不行,你不能因为想简单点而放弃完整性。最后就是简单性了,没有地位。
总结起来就是什么呢,我设计的东西,一定是正确的,从它写下的那一刻,它就绝对正确,为了正确,一致,完整,这个东西多复杂都可以。
而新泽西流派,差不多是将上面说的反过来:简单性是最重要的,所实现的东西,一定要简单,简洁是设计中最重要的考虑因素。 至于完整性,尽量完整就 OK,如果为了完整性而破坏了简单性,那么就可以不涵盖所有的情况。 最后是正确性和一致性,其实不用特别说了,完整性都得不到保证,正确和一致那就只能随缘了。
你可能已经猜到了,更坏的却是更好的就是新泽西流派。
但是看起来,正确性的优先级是远远要高于简单性的。我相信读到这的你会跟我一样的想法,怎么看都是 MIT 流派好呀,why。
而事实就是这样的。
事实就是,无论你有没有编程经验,你可能都会听说过 C 语言,还有 Unix 系统。
但是 Lisp 呢?还有 Scheme 呢?没有编程经验的人基本不会知道这是什么东西,哪怕你有编程经验,没准它们也在你的知识盲区里。
C 语言就代表着新泽西流派,Lisp 就代表着 MIT 流派。
C 语言有这么烂吗?它确实就是那么烂,烂的原因如同上文所说。
但是为什么这么烂的东西,能广泛的被大家使用呢,广泛到这个世界上,已经完全离不开这个东西。
下面是我对 http://dreamsongs.com/RiseOfWorseIsBetter.html 原文的总结,作者以他的理解告诉了我为什么,感兴趣的可以看原文,不感兴趣的就听我说就好了:
原文讲了一个小故事,大致是这个样子的,MIT 流派的一个牛人,对一个问题产生了疑问,他想看看新泽西流派的人是如何在 Unix 系统上解决的,问题是这样的:
一个程序从运行开始到成功结束,往往需要有很多步的操作,但如果在这个过程中的其中一步发生了错误的话,那么就需要保存用户在这个程序执行前的状态,来保证正确性和一致性。 但是往往每一步的调用都是单个的指令,还有可能出现更细粒度的指令(每一步都可能带有其他的一些不透明的指令),所以就不能精准地还原每一步操作。
但 MIT 流派的牛人发现他并没有在 Unix 系统上找到关于这个问题的相关代码,他就问新泽西流派的人是怎么实现的。
新泽西流派的人告诉他,没实现,但是我们意识到了这个问题,假设这个程序出错了,我们还是会将它标记为完成了,但是有时会返回一个错误码,用来告诉你这个程序出错了,你得注意一下。
MIT 流派的牛人不喜欢这种解决方法,因为它不正确,明明它已经错了,你还要进行下去,这不行啊。
新泽西流派的人说,我们觉得这就是正确的,因为我们不想把这个事情搞得很复杂,这个程序在写好之前,你如果发现它错了,那你就多测试几遍,直到它满足你需要的情况不就好了。
然后 MIT 的牛人说:
The MIT guy then muttered that sometimes it takes a tough man to make a tender chicken.
But the New Jersey guy didn’t understand (I’m not sure I do either).
文章作者也不敢说懂了,我同样也不敢说。大家就意会吧。
作者也同时表达了它的看法,为什么 worse-is-better is better,更坏的却是更好的就是最好的呢?
他把 C 语言和 Unix 比作成为了病毒,首先它们很简单,代表着易于编写,同时也便于移植。
而放弃了正确性代表着可以快速的实现我想要的一个功能,说的俗一点,也就是业界所说的,先跑起来,后面再优化(如果你是一个真正的程序员,我相信你能深刻地体会到这句话有多么可怕)。
这些加起来,结果就是你所做的东西就能很快地传播,就像病毒一样。病毒最开始是基本正确的,后面用的人多了,就会产生一些错误,但是因为它很简单,你又能够很快地修复,往往 你只需要保证它 90% 是正确的就够了。
稍后我再说说正确的东西,说一说为什么它们看起来好多了,却没能流传起来。下面这个链接是个知乎回答,有编程经验的且感兴趣的人,可以看一看,当初我也从这里收获很多,也是在说为什么 Worse is Better,非常感谢回答的作者。
为啥 Erlang 没有像 Go、Scala 语言那样崛起? - 布丁的回答 - 知乎
https://www.zhihu.com/question/38032439/answer/84176970
如何保持正确和为什么保持不了正确
再说之前,我想到了之前看过的一篇博客,王垠写的,一个对 Dijkstra 的采访视频。
我觉得在这个视频里面所说的东西能够很好的帮助解释这个子标题:如何保持正确和为什么保持不了正确。
以下是我对博客其中一些部分的摘录,加上我自己的解读:
软件的版本号 2.6, 2.7, … 都是胡扯。本来第1版就应该是最终的产品,可是软件公司总是先弄出来一个不完整的版本,骗大家买了,以后再慢慢“升级”。每次升级都要用户再次付钱。
其实这个观点正式符合了正确性的解释,如果你遵循正确性的哲学,这句话就是对的,可是太理想了对吧,在现世上只存在参考意义,但是也值得我们去思考:
软件的版本号代表着一系列功能的释出,但是你不可否认很多功能都是无用的,很多错误和决策,都是可以避免的,但我们从来不会吸取教训,就像人从来不会从历史中吸取教训一样。
编程有多种流派,我喜欢把它们归类成“莫扎特 vs 贝多芬”。当莫扎特开始写乐谱时,作品就已经完成了。他的手稿一气呵成,书法也很好。贝多芬不一样,他总是在怀疑和挣扎。他的作品一般是还没有想好就开始写,然后就往上面贴纸条修改。有一次贝多芬改了9遍才把手稿完成,后来有人把这手稿一层层的撕开,发现第一版和最后一版是一摸一样的。这种改来改去的做法是 Anglo-Saxon 民族的传统,它贯穿了英国式的教育。
这段话是多么的切合两个流派,紧接着讽刺了一番。我自己是偏向正确性的,所以举一个实际上的例子。
当我曾经去写 golang 的时候,我会奇怪这玩意连个泛型都没有,这怎么用啊,而且写起来也太丑了,满屏幕的 if err != nil
,我写了半天,也没办法在这个语言里找到优雅的写法。
然后我就放弃了。但是后来你可以看到,golang 是在慢慢地进化的,从泛型无所谓的嘴硬到引入这个特性,从稀烂的包管理到正确的包管理做法,它符合了简单性加不断修补。
因为 golang 的编译和部署都可以做到极致的方便,所以它就是能够被广泛流传的,就是 worse-is-better is better 定律。
软件测试可以确定软件里有 bug,但却不可能用来确定它们没有 bug。
讽刺二号,能理解正确性就能理解这句话。
程序的优雅性不是可以或缺的奢侈品,而是决定成功还是失败的一个要素。优雅并不是一个美学的问题,也不是一个时尚品味的问题,优雅能够被翻译成可行的技术。牛津字典对 elegant 的解释是:pleasingly ingenious and simple。如果你的程序真的优雅,那么它就会容易管理。第一是因为它比其它的方案都要短,第二是因为它的组件都可以被换成另外的方案而不会影响其它的部分。很奇怪的是,最优雅的程序往往也是最高效的。
现在我想说这个子标题的后半部分了,为什么保持不了正确。
很多人理解优雅这个词往往会带上艺术的色彩,但是优雅是具有广义性的。优雅是一种做法,是一种习惯,而体现形式却有很多。
我还是举一个例子来表达优雅,很多人编写程序没有抽象和组合的概念,深受简单性的影响,他写出的代码是一条线的,是不能够被中断的。
但优秀的程序是严谨的,就像公式一样,是无状态的,比如 f(x)= y
。给定了输入就会返回输出,它不会影响到其他的公式,也不会出现错误的情况。
在抽象层面上,程序就是许多个公式的组合,比如我可以写下一个 compose 函数,代表公式的组合:
let compose f g = fun x -> f (g x)
compose 函数的意思就是:接受公式 f 和 公式 g,返回一个新的函数,这个新的函数接受参数 x,首先将 x 传入到公式 g 中,再将公式 g 的结果传入到公式 f 中,最后返回最终的结果。
你能看出来,公式 f 和 公式 g,它们和参数 x 是无关的。
这就是优雅,如果我把公式 g 换成其他接受 x 参数的组合,compose 函数依然会生效。我如果用类型来定义这个 compose 函数,可以写成如下形式:
compose : (‘a -> ‘b) -> (‘c -> ‘a) -> ‘c -> ‘b
('a -> 'b)
代表 f,('c -> 'a)
代表 g,'c
代表 x。f (g x)
使用 g 将 x 首先变为 ‘a,然后使用 f 将 ‘a 变为 ‘b,结果就是 ‘b。
这在编程的领域叫做类型推导,结合这篇博客,类型推导带来的就是正确性,只要满足类型推导,那么程序一定就是正确的。
但是优雅很难,难在哪呢?
为什么这么少的人追求优雅?这就是现实。如果说优雅也有缺点的话,那就是你需要艰巨的工作才能得到它,需要良好的教育才能欣赏它。
为了追求优雅,首先你要懂得简单性的东西,至少你需要用过它们。
然后你必须还要接触正确性的东西,你才能理解为什么简单性不好,简单性为了存在就必须不断借鉴正确性的东西来修复自己。(这就是为什么后世的很多语言和程序都在不断的引入“新的”特性,其实新的特性早就在正确性的东西中出现过了)
当你能明白两个流派各自坚持的哲学之后,你需要做出抉择,你更倾向哪边?
我选择了正确性哲学,然后我需要在新的领域中抛开你之前的所有认知,从头学习。因为想要写出正确性的程序,你要付出的工作,是要远大于随便写一个能跑就行的东西的。
我是一个前端工程师,但是我基本没有很特意的学过 JavaScript(前端工程师标配语言,必须会),我的第一个正经学习的语言是 Clojure
,一个 Lisp dialect,我从这里接触到了抽象与组合。
后来我又学习了 Rust,看到了它为了正确所作出的努力,有编程经验的人应该都不会觉得 Rust 很简单,但其实如果你的思维很严谨,并且你很注重细节,Rust 还是蛮简单的。
再后来,我又看了 OCaml,这是一个更加严谨的语言,涉及到太多技术相关的东西,比如其中惰性与非惰性,纯函数与非纯函数等等的概念就够钻研好几年的了,这里就不过多阐述了。
而 JavaScript,正式一个处于中界点上的语言,你可以把它写的具有正确性,你也可以把它写的具有简单性,这也是它最具争议的地方,但是我觉得这也是它想要变革的地方。
所以即使我不怎么学这个语言,我也可以把它写的很好,因为我会以正确性的方式去书写程序。
我相信无论你有没有编程经验,你都可以看到我上面所描述的东西需要很艰苦的工作才可以做的很好。
也可以理解到保持正确性是一个很难的事情。
但是也不要觉得这是一个好与坏的事情,简单性和正确性不代表着好与坏,只代表了不同的哲学。
你用了 20% 的努力,换取了 80% 的成果,这很正常。但是剩下的 20% 的成果,需要你用 80% 的努力去换来。虽然做到 100% 是不可能的,但是你可以做到无限趋近于 100%,而不是 90%。
在合适的地方应用合适的哲学,在应该下功夫的地方下功夫,才是我们需要认真思考,认真去做的。