用 Elixir 的一周

大约一周前我开始学习Elixir. 关于这个我也只是有些模糊的印象但还没有仔细去看。

但在Dave Thomas 出版了 Programming Elixir之后一切都发生了改变.  Dave Thomas 帮我修订过Erlang这本书并且是Ruby的倡导者, 只要Dave对什么产生了兴趣那绝不是空穴来风.

Dave 对Elixir很感兴趣, 在他的书里这样写道:

I came across Ruby in 1998 because I was an avid 
reader of comp.lang.misc (ask your parents). I 
downloaded it, compiled it, and fell in love. 
As with any time you fall in love,
it’s difficult to explain why. 
It just worked the way I work, 
and it had enough depth to keep me interested.

Fast forward 15 years. All that time I’d been 
looking for something new that gave me the same feeling.

I came across Elixir a while back, but for some 
reason never got stuck in. But a few months ago I 
was chatting with Corey Haines. I was
bemoaning the fact that I wanted to find a 
way to show people functional programming concepts 
without the kind of academic trappings those books 
seem to attract. He told me to look again at Elixir. I
did, and I felt the same way I felt when I first saw Ruby.

我能体会.纯粹的感官体验. 就像我知道一件事是对的但还不知道原因,几周甚至几年后这个问题总能回答出来. Malcolm Gladwell在Blink: The Power of Thinking Without Thinking一书中曾探讨过这个问题. 某些领域专家们总能凭直觉判断事情的正确与否,但却给不出具体原因.

但我发现Dave 的描述后,我很想知道为什么他会这样.

无独有偶, Simon St. Laurent也出了本Elixir的书. Simon的 Introducing Erlang… 一书表现不俗,我和他还通过邮件沟通过几次,还有有些熟悉的. 从Pragmatic Press 和O'Reilly 出版社都在争着出版Elixir可见一斑。关于Erlang VM, 我确实一窍不通。

我给Dave和Simon 发了封邮件,之后他们借给我了样书,现在可以开始阅读了 …  谢了 …

上周我下载了elixir 开始学习…

没多久我觉得就上手了. 确实好东西. 有趣的是Erlang和Elixir 实际上是同源的.事实上也确实这样,他们都会被EVM (Erlang Virtual Machine)编译 - 大家管这个EVM 叫 “Beam” VM 但为了和JVM区别就管他叫EVM 吧.

Erlang和Elixir为啥有相同的“语意”? 这得从底层谈起. 垃圾回收, 独立并发机制, 错误处理和代码装载机制都是一样的. 还有: 他们都运行在相同的VM里. 这也是Scala 和 Akka区别于 Erlang的原因. Scala和Akka是运行在JVM 上的, 垃圾回收和代码装载机制从本质上就不同.

Elixir最明显的特点就是语法的表示,和Ruby很像.  简单易懂,还有很多外部资源。

Erlang’s 的语法源自 Prolog 还受到smalltalk, CSP 和功能性编程语言的影响. Elixir 受到Erlang 和Ruby的影响. 类型匹配, 功能优先级错误处理机制都是从Erlang来的.sigils, 语法简写从Ruby 来。 当然也有自己的创新,  |> 管道操作, Prologs DCGs 和Haskell monads (简化了一些类似于Unix的管道操作) 宏引用,是从lisp quasiquote得来,还有逗号操作符.

Elixir更新了AST机制 , 和Erlang AST独有展示的模式不同, Elixir AST 统一了模式这使得meta-programming 更简单.

实现起来也中规中矩,只是有几处需要注意. 字符串插入 (主意不错) :

IO.puts "...#{x}..."

获取x并按格式打印出来. 但只对简单模式的x起作用.

 这可以通过从Elixir调用Erlang的方法来弥补

IO.puts “…#{pp(x)}…” 就可以. 只是需要把 pp(x) 改成

def pp(x) do 
    :io_lib.format("~p", [x])
    |> :lists.flatten
    |> :erlang.list_to_binary
end

Erlang 描述如下:

pp(X) ->
  list_to_binary(lists_flatten(li_lib:format("~p),[X])))

这和Elixir的描述一样. Elixir的写法也更容易阅读. |> 操作符用来把io_lib:format 的结果输入到 lists:flatten 然后再到list_to_binary. 跟unix的管道符|一样

Elixir区别与Erlang在 - 变量可复用.  结果集始终可以表示为static-single-assignment (SSA) . 但在循环结构里千万别这么做.好在 Elixir只用了递归而没有循环结构. 如果循环结构里引用了可变的参数,那远端EVM就没法编译了. 当在SSA顺序结构中使用变量时, EVM 就知道如何处理.循环结构Elixir就没办法了. 其根源可追溯到LLVM汇编- 那就是另一个问题了.

编程语言设计的三定律

有些语言在一些方面做得很好,它们正确,优雅,易于理解,但没人不辞麻烦地提及这些。

错误的地方非常糟糕。你成了笨蛋,如果好处比重大于坏处,你可能被原谅。那些你想在以后消除的坏处,却因为向后兼容性或者是有些傻子(或曰你的狂粉)已经用所有那些坏处写了无数行代码等原因,而不能动。

难以理解的内容是真正倒霉的事情。你必须一遍又一遍地解释,直到你吐血,可还是有些人永远不懂,你必须写上百邮件和数千文字来一遍又一遍地解释这些内容是什么意思以及它们为什么会如此。对于一个语言的设计者或作者来说,这是一个痛苦的深渊。

我将要提到的几件事就是我认为落入这三类情况中的。

在我开始前,我要说 Elixir 做对了好多好多的事情,好处远远大于坏处。

关于 Elixir 的好处是,及时改正它的坏处还不算晚。这只能在无数代码行被写下和众多程序员开始使用它之前才能做到——所以只有少数时间来解决这些问题了。

在源文件中没有版本

XML文件总是这样开始的:

  <?xml version="1.0"?>

这样很好。读取XML文件的第一行就象是听拉赫玛尼诺夫的第三钢琴协奏曲的第一小节(译者注:指其富有辨识度)。这是一个令人赞叹的经验。赞美XML设计师,愿他们的名字得到荣光,给这帮伙计一些图灵奖吧。

把语言的版本放入所有源文件中是必要的。这是为什么呢?

早期的Erlang没有列表推导式。假设我们对一个新版的Erlang模块用一个旧的Erlang编译器去编译。新版的代码含有列表推导式,但旧的编译器不知道列表推导式,所以旧编译器会认为这是一个语法错误。

如果加上版本号, version3  的 Erlang 编译器碰到这样开始的文件:

-version(5,0).

它会知道是一个更高版本的文件,并可能这样提示:

**  天~~~~啊~~~~~~  **

   哦,烦炸了,我只是第三版的编译器,看不到未来的变化。

   你刚刚给我一个第五版的程序,这说明我在地球上的寿命已过。

   你将不得不杀掉我,把我卸载,然后安个第五版的新编译器。曾经玉树临风的我现在没了价值,我将不再存在。

   再见吧,老朋友。

   我感觉头痛。我要休息一下……

**

这是数据设计的第一法则:

 所有未来可能会改变的数据应标记有版本号。

模块就是数据。

Funs and defs用法不同

在写 “Programming Erlang” 一书时Dave Thomas问function为什么不能写到 shell里.

如果代码这样:

fac(0) when N > 0 -> 1;
fac(N)            -> N* fac(N-1).

直接复制到shell里是不能运行的. Dave觉得这样很奇怪.

Lisp里这样做是没问题的. Dave 觉得这很会让人迷惑-确实如此。估计论坛里里关于此的话题也会很多.

我解释这个问题已经无数遍了,从黑发到白发那么长的时间里都在解释.

根源就是Erlang的一个bug.

Erlang的模块是一系列的 FORMS.

Erlang shell解析的是一些列 EXPRESSIONS.

但Erlang的 FORMS 不是EXPRESSIONS.

double(X) -> 2*X.            in an Erlang module is a FORM

Double = fun(X) -> 2*X end.  in the shell is an EXPRESSION

上面两个是不同的.这可能是Erlang一个永远的痛,但我们也会接受的.

在Elixir模块可以这么写

def triple(x) do
   3 * x;
end

估计很多人都会直接复制到shell里直接运行

ex> def triple(x) do 3*x; end
** (SyntaxError) iex:66: cannot invoke def outside module

如果你不知道这个根源可能就要花费大量的时间去弄明白问题的本质 - 就像 Erlang.

修复这个问题很简单.以erl2为例.Erlang里面没法修复这个问题 (版本兼容问题) 所以就用 erl2. 改一下 erl_eval .

 FORMS不是expressions, 所以加了个关键字def

 Var = def fac(0) -> ; fac(N) -> N*fac(N-1) end.

这就定义了 一个有副作用的expression . 这样shell就能处理了

副作用指的是需要创建一个 shell:fac/1 功能(就像于预先定义的模块一样).

iex> double = fn(x) -> 2 * x end;

iex> def double(x) do 2*x end;

上面这两个是一样的, 要定义一个Shell.double

修复这个问题就能高枕无忧了.

函数名称中有额外的句点

iex> f = fn(x) -> 2 * x end
#Function<erl_eval.6.17052888>
iex> f.(10)
20

在学校里我学会了写 f(10) 来调用函数而不是 f.(10) ——象是Shell.f(10) 这样名称的函数是“真的”(译注:或说是可以接受的)(这是一个在 shell 中定义的函数)。shell 部分是隐式的,所以它可以只用 f(10) 来调用。

如果你就这样把它放这,那就期待用你生命的下个二十年去解释为什么吧,期待在数百论坛各自中的数千封邮件吧。

send操作符

Process <- Message

这是什么? 知道把 occam-pi 转成Elixir有多难么.

send操作符应该是这样的 !, 否则就没法用occam-pi 了:

Process ! Message

这需要花时间去把 <- 当做 ! - 这意味着基库的重构 每当发送一条消息到队列时,默认操作会加上 ! 之后就执行回退操作<-.

这是一个语法问题。让人爱恨交织的语法。如果10分制的评级标准,10代表最烂,1代表不错的话,这个问题我给3分。

这会使想转到Elixir的Occam-pi 程序员更加难以抉择,什么,只需要简单地使用!就能完成<-的功能?这可真是出人意料啊。相信会有很多人受到鼓舞的

管道运算符

这是一个很好很好的东西并且很简单就能掌握,以至于没有人会给你报酬,这就是命。

这是来自Prolog语言的基因. 在Prolog中的基因是显而易见的, 但是在Erlang中确实不明显的(son-of-prolog) 但是又在Elixir (son-of-son-of-prolog)中显现了.

x |> y 意味着调用了 x 然后获取了 x 的输出并且将它作为 y 的另外一个参数(第一个参数).

所以

 x(1,2) |> y(a,b,c)

意味着

 newvar = x(1,2);
 y(newvar,a,b,c);

着非常有用。假设我们要把握的是把一个变量 ‘abc’ 转换为 ‘Abc’. 在Elixir中没有利用的函数但是还有一个功能,就是去控制一个字符串. 所以我们需要现将这个变量转换为string,在Erlang中,我们这样写。

 capitalize_atom(X) ->
    list_to_atom(binary_to_list(capitalize_binary(list_to_binary(atom_to_list(X))))).

这样写太业余了,所以,所以我们还可能会写成这样。

 capitalize_atom(X) ->
     V1 = atom_to_list(X),
     V2 = list_to_binary(V1),
     V3 = capitalize_binary(V2),
     V4 = binary_to_list(V3),
     binary_to_atom(V4).

但是,这更糟-恶心的代码. 我都不想再看到他了。

于是 |> 来了:

    X |> atom_to_list |> list_to_binary |> capitalize_binary 
      |> binary_to_list |> binary_to_atom

为什么我回调用这个隐藏的方法?

Erlang 从 Prolog 中演化而来, 而且 Elixir 也继承了 Erlang.

Prolog 有 DCGs ,所以

  foo --> a,b,c.

得到了扩展成

  foo(In, Out) :- a(In, V1), b(V1,V2), c(V2, Out).

这基本上是同样的想法。我们创建了一个额外的隐藏参数的方法调用线程的方式在函数调用序列的进出。这是一种典型的Haskell方式,但请保守这个秘密。

Prolog 有 DCGs, Erlang 没有, Elixir 有管道操作符!

Elixir 还有sigils

Sigils很好,我们要加到Erlang里.

字符串是一种抽象. 编程语言通常使用双引号圈住它们.就像这样:

x = "a string"

编译器在按照语意将其转换.

在Erlang里

 X = "abc"

代表 “X 是ASCII中a,b,c的整数表示”

但也可能自定义.Elixir里 x = “abc” 代表x 是 UTF8 字符集y. 通过在最前面加上r可以改变默认集, Erlang里这样写:

 X = r"...."

代表编译过的正则表达式, i.e.  和 X = re:compile(“….”)一样 -将已知字符解释为想要的不同模式.下面代码:

 A = "Joe",
 B = s"Hello #{A}".

B值是“Hello Joe” - s代表 “替换当中的字符”.

Elixir在这方面做得很好,有很多不同的sigils.

Elixir的sigil语法不太一样,如下:

%C{.....}

{}[]接上C.

Sigils确实不错. Erlang 15年前就有这个功能,现在引入也不会有什么兼容性问题.

Docstrings

Docstrings真好.

但有个小建议. 把docstring放到功能定义.

如此

@doc """
 ...
"""

def foo do
   ...
end

这般引入:

 def foo do
   @doc """
   ...
   """
 end

否则会出“detached comment”: 代码会出错.注释会在调用功能时断开.

Erlang里我没法分出注释是上一个功能还是下个功能的. 最好就是写到对应的功能模块内部.

defmacro 引用

好东西. 这是解析模式的转换. 好处是无需关注其实现,引用帮你解决问题了.

这就是那些妙不可言的东西之一 - 需要解释一下. 就像Haskell的 monads  - Yup, monads确实很容易解释,难怪需要有那么多文章解释它有多好用(其实不简单).

Elixir宏很简单 -就是lisp的quasiquote和comma operator - 简单吧)

强调符

像这样:

iex> lc x inlist [1,2,3], do: 2*x
[2,4,6]

不是这样:

iex> lc x inlist [1,2,3] do: 2*x 
** (SyntaxError) iex:3: syntax error before: do

后面的冒号让人迷惑.

空格符

iex> lc x inlist [1,2,3], do : 2*x
** (SyntaxError) iex:2: invalid token: : 2*x

应该是“do:” 不是“do :”

空格就是空格. 字符串里面不能随便用,但除此之外即便为了美观我也会经常使用空格符.

但 Elixir不行 - 让人不喜欢.

Closures完全一样

Elixir (fn’s)的Closures 和 Erlang (fun’s)一样.

fn能捕获在其功能范围能的任何变量值,这一点非常有用 (ie 创建恒定 closures) . JavaScript 这块做的很不好。 一个JavaScript 和 Elixir的对比:

js> a = 5;
5
js> f = function(x) { return x+a }; 
function (x){return x+a}
js> f(10)
15
js> a = 100
100
js> f(10)
110

功能 f错了. 定义的f, 开始使用. 重定义 f就不行了.函数是编程的好处就是使编程变得简单.如果 f(10)的值是15,就不应该在变来变去了。

Elixir呢? 没问题:

iex> a = 5  
5
iex> f = fn(x) -> x + a end
#Function<erl_eval.6.17052888>
iex> f.(10)
15
iex> a = 100
100
iex> f.(10)
15

正确的closures就应该指向恒定的数据地址 (就像Erlang) - 而不是数据的可变部分. 如果closure里有指向可变数据的指针,后面改动了数据就会破坏 closure的一致性. 死锁互斥问题.

一般使用closure的代价很高,需要复制环境里的所有变量深拷贝,但 Erlang 和Elixir不是这样, 数据都是恒定的.可以用的时候再去找.也是通过指针实现的 (内部操作) 垃圾回收机制会移除所有没有指针关联的数据么,避免空指针.

closures可以写到 shell, 但不能写到模块里.

如果写成这样

a = 10;
f = fn(x) -> a + x end;

在shell里

为什么不能这样呢?

a = 10;
def f(x) do
   a + x
end

在erlang2是可行的.

最后

这是我开始Elixir的第一周, 确实没让人失望.

Elixir 语法简洁并融合了Ruby和Erlang的长处. 还有自己的创新。

这是门新的语言, 但介绍的书却不是同步的. 第一本介绍Erlang 的书在Erlang 被发明后7年才出现, 畅销书更是在14年后才出现. 用21年去等一本真正的介绍书籍,代价太大.

Dave很喜欢Elixir,我也觉得很酷,我想我们会在使用过程中找到更多乐趣的.

Erlang在占世界二分之一的手机网络中提供了抢到的支持,像是WhatsApp. 在简化版出现后,会有更多的新鲜血液加入这个阵营,我想以后的发展一定更加有意义.

这是篇即兴短文. 也许会有些不妥之处,欢迎大家指正.