elixir:灵丹妙药?or 徒有其名?

2018-03-28 15:22:03 浏览数 (1)

13年的时候正在追Erlang,有天看见Joe老爷子的一篇博客介绍Elixir [1],才第一次听到这个语言。

This has been my first week with Elixir, and I’m pretty excited. Elixir has a non-scary syntax and combines the good features of Ruby and Erlang. It’s not Erlang and it’s not Ruby and it has ideas of its own. It’s a new language, but books are being written as the language is being developed. The first Erlang book came 7 years after Erlang was invented, and the first popular book 14 years later. 21 years is too long to wait for a decent book. Dave loves Elixir, I think it’s pretty cool, I think we’re going to have fun together.

这评价从Joe老爷子嘴里吐出实属不易 —— 颇有点拍拍 Jose Valim 的肩膀,说「小伙子,努力干,偶看好你噢」的赶脚。噢,我忘了介绍,Joe老爷子是Erlang的创始人,Jose是Elixir的创始人(之前是Rails的core member),两人看上去相差三十岁。去年的文章我曾经讲过程序员不得不看的几本编程书,老爷子的 "Programming Erlang: Software for a Concurrent World",就是其中不得不看的一本(现在有2nd edition了)。好的编程书重在讲语法背后的思想,设计背后的初衷,如果单纯是要了解语法那些肤浅的东西,看看 "Learn X in Y minutes" 就好。所以大家看编程书的话,最好看语言作者的书,实在没有,也要看社区里的大牛的 —— 因为他们聊的不是语法(当然也不是寂寞 ^_^)!

扯远了,那时候把玩了一下Elixir,就像『七周七语言』那样,玩两周就算了,没有深入。唯一的感觉是:哇,BEAM [2] 上总算有一个让人好好写代码的语言了 [3]。

两年多的时光弹指过去,Elixir在最近终于发布了1.0.x版本,而Jose本人又频频上镜,到处布道Elixir,我才重新关注起这门语言。一门社区驱动的语言(或者框架),在没有到1.0之前,都意味着语法和库函数的极不稳定。1.0以后,起码意味着你可以拿它写点什么,而不至于写下的代码半年后就完全没法工作 [4]。所以我就重新拾起Elixir的文档,边啃边写。

差不多一个月下来,写了二三十个小项目,从ip packet的parsing,到http reverse proxy,都是几百行以内,一两个小时顶多到一整天能搞定的东东。借着这股兴奋劲,我来讲讲自己对Elixir的浅显认知。

惊艳的语法

Elixir的语法在向Ruby致敬,同时透着Erlang和Prolog的灵气。任何语言语法的设计都和其创始人的偏好和目标分不开,Ken Thompson/Rob Pike的golang看上去很C,Jose Valim的Elixir自然就很Ruby。当然,植根于Erlang的Elixir,又有有很多自己的特点。

最让人爱不释手的是pipe |>,它让你把一层层的逆着你的思维的函数调用变成了更直观的表现,比如说我们常常这么写代码:

代码语言:javascript复制
IO.puts(tabularize(to_map(Store.get_host(host))))

或者

list_data = Store.get_host(host)
map = to_map(list)
formatted_output = tabularize(map)
IO.puts(formatted_output)

这样的代码在Elixir中可以被写成:

代码语言:javascript复制
host
|> Store.get_host
|> to_map
|> tabularize
|> IO.puts

非常清晰 - 最重要的是,它更符合你的思维模式,让代码更容易在指尖流淌。我们写代码的时候,基本就是一个不断「分治」的过程:把大问题分解成小问题,小问题分解成更小的问题,最终解决问题。而Elixir让你的代码和你的思路高度一致。

这个语法特点来源于Prolog,遗憾的是,继承自Prolog的Erlang没有将其捡来,却把它遗给了继承于Erlang的Elixir。

看到这里,有同学也许会问?这不是object chaining么?老娘/老子在Ruby里,或者在jquery中,经常这么写代码。。。

虽然pipe和chaining表述代码的方式有些类似,但背后的思想不太一样。chaining是在对象上不断执行其方法,类似于语法糖,而pipe是把上一次的执行结果传递给下一个函数的第一个参数,和unix的pipe类似。chaining的限制很大,为此你要牺牲方法的特性 [5],而pipe非常灵活,你可以一边组织思路一边组合函数,有点搭积木的节奏。

其它的语法细节,如函数式编程,sigils,first class doc等等,就不提了,感兴趣的可以自行了解。

Pattern matching

我们知道,Erlang在concurrency以外的另一大特点是pattern matching,它能让你把绕来绕去的if/else变得简单明了。如果你不幸在工作中遇见if/else hell,再看过支持pattern matching的语言,你一定会泪流满面。

那么问题来了,当pipe遇见pattern matching是什么光景?看下面的代码:

浅显易懂,还很难有逻辑错误。这个代码里同一个 run 被定义了很多次,根据参数的不同,会调用不同的函数。我们再看一个例子:

除了pipe,里面用到了pattern matching recursion,这里还是分治的思想。它是Elixir下写代码的一个很自然的模式:任务不断拆解,每个函数专注只干一件事。当然,几乎所有的语言都希望开发者这么做,但不少都没有提供正确的工具让开发者自然而然这么做。

使用pattern matching取代大部分条件分支是件相当伟大的事情:代码的简洁自不必说,其效率还有可能进一步优化。ifelse是一种顺序执行的逻辑,因为其语法结构的灵活(if的条件里是个函数这事大家都干吧),顶多是对一些特殊的情况使用跳转表优化,大多数情况是O(N),而且很难并行处理。而pattern matching由于其语法上的限制,很多情况可以被优化成decising tree,时间复杂度是O(logN),而且未来还有并行处理的优化空间。

当pattern matching遇见macro

当然以上的好处也是erlang的好处,但Elixir在此基础上做了一件也许是跨时代的事情:支持macro。Ruby也支持macro,任何从lisp演进或者接受lisp思想的语言也支持macro,为什么Elixir支持macro如此特殊?目前已有的支持macro的语言,macro更多地被用作突破语法的极限 —— 要么用于定义DSL让代码简洁,如rails;要么用于生成繁杂的接口代码而不必手工撰写。但Elixir在BEAM上支持macro,不管是有心还是无心,跟pattern matching一配合,带来了无穷的想象空间。

Elixir的unicode的大小写转换不必再提,我在「颠覆者的游戏」一文已经介绍过。类似的问题都可以这么处理。比如说我昨天做了一个中文简繁转换的模块:把wikipedia的最新词库导入,使用macro在编译时生成近10,000个按词进行正向最大匹配的递归函数,代码却仅需200行(见 github.com/tyrchen/chinease_translation,如果觉得好,请在github点赞)。以此类推,中文短句的slugify也就是同等规模的问题。

还有数据清洗和数据过滤。比如说众所周知的敏感词过滤。敏感词词库一更新,只需要重新编译出新的代码,加载即可(BEAM支持hot code reload)。

再讲一些做系统的新思路:

  • ✓ 用户名保留。使用一个文本字典,记录要保留的用户名。用macro生成pattern matching的代码。
  • ✓ 弱密码防护。把黑客用于攻击的常见字典编译出一个密码黑名单的pattern matching的代码。
  • ✓ 文本分析。对于格式各异的日志文件,定义抓取范式,然后通过这些范式生成pattern matching的代码。

等等。它们共同的特点是把原来依赖于数据库才能完成的事情,交给了编译时完成。花了很小的代码,我们就享受运行时的高效,还有组件化,没有外部依赖等等好处。我还没有具体测试过对于某种pattern,生成的函数超过10k级别的时的BEAM的处理效率,但在10k及以下的pattern,效率非常非常高。

天生的concurrency支持

这个就不多说了,Erlang的基于actor的并发模型,let it crash的处理思想,supervision tree,error kernel,都是在二十多年来与并发作斗争过程中不断总结出来的best practice,无论在思想上,还是实操上,在可预见的未来,没有语言能够超越它。Elixir站在巨人的肩膀上,坐享其成。

服务周到的工具链

进入21世纪以来,新兴的语言都在工具链上卯足了劲,工具链(几乎)成为语言的一部分(一起ship),而非附属品。从产品设计的角度上,这是非常英明的 —— 语言间的竞争如此激烈,光盘儿正条儿顺是不够的,得舍下身段做丫鬟 —— 谁把程序员哄的开心,谁胜出的几率要更大些。

你仔细想想golang的 go xxx(test/fmt/get),nodejs的npm,就大体知道我想讲什么了。作为一个C程序员,为了使用一个lib所犯下的折腾,让我感觉自己活在史前文明;而作为一个python(java/ruby)程序员,过度繁荣(各有优劣)的工具链市场又让我在适配上花太多时间 —— 三宫六院七十二妃,就是不如一个琴操姑娘好啊。

Elixir自身携带了mix —— 从项目的创建和scaffolding(mix new),编译(mix compile),到测试(mix test),到文档(mix doc),到依赖管理(mix deps.xxx),全部包圆,还有其它语言自带的工具如此全面么?据说以后还要把release和deployment管理起来,嗯,很好很强大。

总结

做硬件的兄弟总是嘲笑我们这些写软件的笨蛋们 —— 当他们做的硬件能够不断以搭积木的方式自我累积,数十亿个晶体管组成的复杂系统可以bug free时,我们写的软件却糟糕得一塌糊涂。原因有多方面的,如果从语言本身出发找原因,那就是语言本身并未让你以搭积木的方式组织系统。

那么,什么样的语言更容易贴近搭积木的组织方式呢?

  • ✓ 提倡使用递归(递归就是以自身为积木)
  • ✓ 以pattern matching的方式组织代码(每个代码快尽可能小,只处理一件简单的事情)
  • ✓ 语言层面提供解耦的工具(如erlang的process,golang的chan,scala的actor)
  • ✓ 系统的一部分损坏,并不影响未损坏的部分

嗯,Elixir是灵丹妙药,还是徒有其名?让时间来说明一切。就这么多(真没想到这篇文章断断续续写了快一周了)。


1. 见:http://joearms.github.io/2013/05/31/a-week-with-elixir.html

2. Erlang的VM

3. 初学者在Erlang的世界里很容易找不到北,这个,走过这段路的人都有感受

4. 这一点,我在meteor下吃了大亏,我的teamspark写于0.5.x,然后每一次版本升级,就各种crash…

5. 比如说本来可以返回一个结果,却不得不返回自己,而把结果存储在对象中

0 人点赞