十年学会函数式编程:入门的最差实践

不知道大家有没有类似的体验,有些概念如果直接告诉你它是什么,无论如何都理解不了。但是如果再告诉你它不是什么,可能就恍然大悟了。同样的,有些事情告诉你怎么做(Dos)往往不如告诉你别怎么做(Don’ts)来的更有效。回顾我学习FP的历程,确实存在这样一些最差实践,如果避开这些坑,入门应该是不需要十年的。

十年学会函数式编程

Peter Norvig 大神多年前写过一篇 Teach Yourself Programming in Ten Years,阐述了为什么学习编程是持久战,没有捷径可以走,以及应该怎么去学习编程,每个程序员都应该看一看。里面有句话讲得特别好(不仅仅局限于编程领域):

The key is deliberative practice: not just doing it again and again, but challenging yourself with a task that is just beyond your current ability, trying it, analyzing your performance while and after doing it, and correcting any mistakes. Then repeat. And repeat again.

Peter文章的“十年”是个夸张的说法,主要是为了强调学习编程没有捷径,引导大家摆脱急功近利的心态,他学编程当然不需要十年。我的这篇文章标题差不多,就多了一个Functional,但我是真的用了十年的时间才学会的,而且顶多只能算是入门而已。所以对我而言十年并不是夸张的说法,是智商真的有限。

其实函数式编程(Functional Programming,以下简称FP)并不神秘,只是很多人把它吹得过于神化了。

Protocol Buffer高阶玩法:如何优雅地批量修改proto文件

每个程序员少不了和Protocol Buffer打交道。Protobuf现在几乎已经是业界标准了,设计rpc接口协议不会考虑其他的选择。可能也有人喜欢造轮子发明自己的一套协议,但是请相信我,造这种轮子除了可以用来汇报之外(前提是先说服老板允许你浪费时间造轮子),基本上不会给公司业务、后台服务上带来什么效率的提升。Protobuf有诸多家喻户晓的优点,比如广泛的支持各种语言、具有良好的版本兼容性、体积小解析快等等。作为后台协议这些特性可以说是基本的要求。但是protobuf之所以这么优秀,我认为还离不开它丰富的api和可扩展性。

Why Emacs?

Emacs已经陪伴我十几年了,称得上是第二个人生伴侣,只要开电脑就习惯性地打开Emacs。内心由衷地感谢Richard Stallman,开发出这样一款编辑器,给平淡无奇的程序员生活带来许多乐趣,是时候给Emacs写一篇软文了。Emacs诞生于1972年,可以说是相当古老了。现在已经2021年了,这个老古董还能继续用吗?

Done is better than perfect

又发现了自己的一个缺陷,不管做什么事情都像绣花一样慢,事情做的是不错,但是效率极其低下。比如,博客文章从开始写到发布至少要经历两三周,各种精雕细琢,但就是迟迟不能发布,总觉得还少了点东西。就是追求完美,一定要做到无可挑剔才行。实际上完美是一种永远都不可能达到的状态,从0到80分很容易,从80到99的时间是指数上涨的,从99到100,一定要耗费巨大的精力和时间。

逃离舒适区

所有生物都是趋利避害的,人也不例外,本能的都想让自己更舒服一点,少遇到一些挫折和痛苦。“不要停留在舒适区”的道理大家都懂,我也从来不认为自己还在舒适区。最近反思了一下,发现舒适区比我的认知要大很多,我以为自己走出去了,其实并没有,还是在这个圈子里面,这比心甘情愿留在舒适区还要可怕。今天来深刻反思下我犯过的一些错误。

该不该用-webkit-font-smoothing?

跟InfoQ中文站、36kr这些国内的站点对比了下,发现我博客上的字体非常的别扭,尤其是在外接显示器的情况下。但是看用的字体也是一样的,都是PingFang SC,为什么呢?于是把css一行一行的拷贝过来测试,最后发现是下面这两句导致的差异。

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

之前看起来字体非常的粗,都挤到一起了。加上这两句,体验好多了,变得异常的清晰。在Retina屏幕上变化不算明显,但是在2k分辨率的外接显示器上,完全是两种体验。

体验问题解决了,还想再深入了解下原理,看看MDN上的介绍。这两个样式都不是css标准样式,而且仅在Mac下生效。font-smooth曾经是标准的,但是后来被移除了。MDN上有红框警告,建议在生产环境不要使用该样式。看到这个警告,内心开始产生怀疑了,用这个是不是有什么坑呢?

又搜索了下,发现有人在google font的仓库里面提issue,希望去掉google font页面的-webkit-font-smoothing样式,因为这会导致字体与实际的表现不同。作者也出来回应了,“谢谢你的建议,但我们还是要保留,因为要以最好的状态来展示这些字体”

另外还看到一篇文章建议不要再使用,Please Stop “Fixing” Font Smoothing。里面提到了,让浏览器自己选择才是最好的,次像素防锯齿才是最好的,即便开了antialiased看起来更舒适,但那也是不对的。说了一大堆道理,恳求大家不要再用了。讲真的,我差点就信了。为了确认下作者是不是在说实话,我打开Inspector检查了下,看到了下图。

幸好没盲目的听作者的。文章比较老了,不确定是不是作者写完文章后来又改变了想法把它加回去了。即便如此,那也应该更新下的。这篇文章出现的频率还是很高的,不知道会误导多少后来的人。其实有很多官方的Guideline也存在同样的问题,比如苹果的开发者文档,经常文档里面说一套,官方的app又不遵守。所以这些规则和最佳实践可以去参考,但是没必要盲从。还是要结合自己的具体场景做出选择。

顺便看了看其他网站,基本上都有使用,比如苹果的开发者文档、MDN文档。所以放心大胆的用吧,没有任何的副作用,这才是最佳实践。

Clojure开发初体验

先说说我为什么想试下Clojure。作为Emacs的忠实用户,有时需要用Emacs Lisp实现一些功能,但是Emacs Lisp的文档写的比较简洁,网上相关资料也非常少。很多时候都不了了之了。后来发现,有些基本概念是Lisp系语言共有的。搞不懂的问题,可以查查其他Lisp语言的资料,或许能有答案。

因此萌生了系统性学习Lisp语言的想法,想看看冰河翻译的Practical Common Lisp。但是如果看完只能解决我使用Emacs的问题,好像成本有点高了,毕竟Common Lisp不能拿来干活啊。

除了Common Lisp还有别的选择吗?当然有,那就是Clojure了。做为一门Lisp方言,Clojure有着优雅的语法和强大的表达能力,最关键的是真的可以拿来干活。跟Scala、Kotlin一样,Clojure也是基于jvm实现的,这意味着有大量成熟的Java生态的工具可以直接使用。

书籍

第一个步骤肯定是找些书来看看了。经过我的搜索,这些书都还不错,可以挑一些来看看。

打王者荣耀,我学到了什么

最近这段时间王者荣耀打的比较多,大体计算过,每天在线时长要4个小时,非常的颓废。不过好在现在已经戒掉了。游戏本身其实没什么好讨论的,攻略文章和视频满天飞。今天不谈技巧这些东西,而是分享一些更深层次的体会。

The Perfect Language

如果有一种语言是完美的,那它应该是什么样子的?谈谈我的看法。

防止用户用石头砸自己的脚

C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off.

这句话出自Bjarne Stroustrup。即便C++让搬起石头砸自己的脚看起来更难一点,但并没有从根本上阻止用户这么干。根本问题在于用户是靠不住的,他可能不知道哪些行为是危险的。C++里面有不少这种一不小心就砸到自己的特性。比如指针和引用,每个程序员应该都有被C++的指针和引用伤害过,用的姿势不对就会引发灾难。所以写C++程序一定要小心翼翼,虽然我已经写了很多年了,但还是保持着“敬畏之心”,每次不确定某个写法的时候都要查资料反复确认。还有一些本来就可有可无的特性,引入之后反而让事情变得更复杂,但是又没有带来任何收益。比如i++++i,写个i = i + 1能又多难呢?这都是导致程序员脱发的元凶啊,生活本来可以更简单的。

那应该如何防止用户干这种事情呢?在指针和引用的问题上,Rust的处理方案是非常优秀的,通过ownership和borrow checker完美的解决了这类问题。只要用的姿势不对,编译都过不了。虽然代码写地略痛苦,但是和上线之后才发现问题相比,这点痛苦完全可以忽略不计了。

有问题就应该在编译阶段就解决,运行时再发现已经太迟了。

Maybe, Option, Optional

如果有一个整数型的变量,如何判断它是否有值呢?下面是一种常见的做法。

int write_x();

int read_x(int x) {
    if (x == 0) {
        do_something_without_x();
    } else {
        do_something_with_x();
    }

    return 0;
}

read_x能否正常运行,取决于write_x的实现。如果write_x也一样认为0就是没有的意思,那就万事大吉,否则就会出翔了。假如由两个团队分别维护read_xwrite_x两个函数,出现问题的机率更大。即使某个时刻双方达成了约定,无法通过系统进行约束,约定就是废纸一张。当然可以尝试通过其他途径进行封装,比如protobuf的has_x(),或者是将x的访问统一封装成函数库。只要有机会可以直接操作x,都无法彻底的避免。如果在语言层面支持,结果就非常不同了。

这里需要的是一种类型,可以完美的描述某个东西不存在的场景。这个类型,就是Haskell的Maybe,Scala和Rust中的Option,Swift的Optional。如果用Scala的话,上面的代码应该是这样的。

def readX(optionX: Option[Int]): Unit = {
    optionX match {
      case Some(x) =>
        doSomethingWithX()
      case None =>
        doSomethingWithoutX()
    }
  }

通过Pattern Match,如果忘记了匹配case None,编译器会提醒你。如果不使用pattern match,而是通过optionX.get获取,当x不存在的时候会抛异常,虽然问题发现的晚,但是可以保证不会执行一段奇怪的逻辑。

Option实际上是一种代数类型(Algebraic data type),借助ADT可以实现各种复杂的类型,详细解释可以参考Haskell的wiki