从 Go 开发者的角度看 Elixir 的设计思想

admin 8年前

从 Go 开发者的角度看 Elixir 的设计思想

免责声明: 这篇文章不是带你入门的,我只是把玩了一下这个编程语言,也不是什么专家,就把我写的当做一道开胃菜吧。我只是把我几个小时的调研结果汇总一下,以便能够帮助大家花几分钟读完之后再看 Elixir 是否吸引到了你。

Elixir 是什么

Elixir 是运行在 Erlang 虚拟机 BEAM 上的一门新兴的编程语言。它完全兼容 Erlang,并且拥有共同的组件,但是它提供了类似 Ruby 的语法以及很多的语法糖。由 Rails 核心贡献者 José Valim 建立,它吸引了很多 Ruby 和 Erlang 的开发人员,并且试图结合 Erlang 的强大以及 Ruby 的编程乐趣。

弹性的不可变性

Elixir 试图成为一个严格意义上的面向函数的编程语言,但是有一个例外,就是数据的不可更改,一旦你定义了一个变量,就不可更改。这样做有很大的好处,但是对于像我这样有思维定式的人就需要适应一下。很多情况下你希望将变量用于递归以及函数调用中,所以 Elixir 提供了一种弹性的不可变性,就是你可以重用变量名。

Interactive Elixir (1.1.1) ...  iex(1)> a = 1  1  iex(2)> a = "what?"  "what?"

 这看起来好像改变了变量,而实际上并没有,它只是 a1=1 和 a2="what?" 的一种语法糖。传给第一个变量 a 的数值仍然是 1,你可以认为 Elixir 是动态类型的,并且支持 REPL,而且这也是可以编译通过的。

模式匹配

Elixir 像 Erlang 等很多其它的函数式编程语言一样拥有模式匹配,它是一个杀手级特性,会改变你写代码方式。在 Elixir 中,= 操作符实际上是对它的两端进行匹配。

Interactive Elixir (1.1.1) ...  iex(1)> a = 1  1  iex(2)> b = 8  8  iex(3)> [a, 8, c] = [1, b, "I am a teapot"]  [1, 8, "I am a teapot"]  iex(4)> c  "I am a teapot"  iex(5)> [a, 7, c] = [1, b, "I am a teapot"] # try to match b = 7  ** (MatchError) no match of right hand side value: [1, 8, "I am a teapot"]  iex(6)> [a, 8, c] = [2, b, "I am a teapot"] # rebind a = 2  [2, 8, "I am a teapot"]

这段程序试图逐个进行匹配,直到某一个不对,在匹配的过程中也会绑定变量。模式匹配减少了 if/else 语句的滥用,可以类比为 switch 语句的逐个匹配,但是它又更进一步,你还可以在定义函数的过程中,匹配不同参数的输入。

is_three = fn    {3}          -> IO.puts "yes, this is the number 3"    {num}        -> IO.puts "no, this is the number #{num}"    {num, more}  -> IO.puts "no, those are 2 numbers, #{num} and #{more}"  end    iex> is_three.({3})  yes, this is the number 3  :ok  iex> is_three.({6})  no, this is the number 6  :ok  iex> is_three.({3, 6})  no, those are 2 numbers, 3 and 6  :ok

原子

它类似于 Go 中用 iota 逐个声明的常量,也是在其它编程语言中叫符号的。它用来标记这是个什么,也可以在模式匹配中进行匹配。

iex> :imanatom  :imanatom  iex> {:imanatom, a} = {:imanatom, 10}  {:imanatom, 10}  iex> {:imanatom, a} = {:anotheratom, 10}  ** (MatchError) no match of right hand side value: {:anotheratom, 10}    iex> IO.puts "hello world"  hello world  :ok

很多函数会返回 :ok 原子,用来代表是成功了或者带有信息的 :error,这样你可以结果来进行不同的操作。

Processes 与 Goroutines

Elixir/Erlang 中的 Processes 与 Go 的 Goroutines 是类似的,都是轻量级的线程,而且运行不依赖于系统线程。Elixir 实现了 Erlang 的 actor 模型,它的轻量级线程是可以被直接寻址的主实体,当建立一个 process 的时候,你可以获取到相应的 PID,你可以用 PID 向 process 发送消息,process 能够模式匹配不同的消息,从而判断出这是什么消息以及要做什么。

Elixir actor 模型下的 hello world:

ex(1)> parent = self()  #PID<0.57.0>  iex(2)> spawn_link(fn ->  send parent, {:msg, "hello world"} end)  #PID<0.60.0>  iex(3)> receive do {:msg, contents} -> IO.puts contents end  hello world  :ok

通信的通道对于 process 是通明的,实际上,所有的虚拟机都可以被连接到一个网状网络中,你甚至可以向当前网络环境中其它电脑的 process 发送消息。多个 process 可以捆绑成一个 process 组,你可以向 process 组发送消息,从而让其分配负载。由于网络对于 process 是透明的,那么是否用微服务也就没有那么多争论了,你可以很容易的改变架构。

actor 模型与 Go 的 CSP 模型不同的地方在于,process 在通信的过程中是可以被直接寻址的,而 goroutine 是匿名的,无法被直接寻址。

Supervisors

Supervisors 也是 process,它可以监控你的 process,并且可以在 process 由于某种原因崩溃的时候让其重启。它也可以控制你的部分程序,让其开始或停止。假设我们想监控有一个计数程序,一个类似的用 Go 实现的 supervisor 程序如下:

package mainimport (   "fmt"   "time")func main() {   // Instead of: go count(3)   go supervisor(count, 3)     select {}}func count(to int) {   for i := 0; i <= to; i++ {    fmt.Printf("i=%d\n", i)    time.Sleep(time.Second)   }     panic("pretend that something broke")}func supervisor(fn func(int), args int) {   for {    func() {     defer func() {      if err := recover(); err != nil {       fmt.Println("panic recovered")      }     }()     fn(args)    }()    fmt.Println("restarting process")    time.Sleep(time.Second)   }}

这过于简单了,Elixir/Erlang 的 supervisor 是内建的,而且复杂的多,允许多种重启策略等。他们通常被组合为“supervisor 树结构”,所以你的应用可以弹性的保持各种层级。

结构体与协议

Elixir 的结构体与 Go 的类似,虽然底层都是 map 的语法糖。但是 Elixir 保持函数编程精髓的同时,给其加入了一个面向对象的好处,你可以像匹配 map 的值一样模式匹配结构体内的属性。

Elixir 的协议如 Go 的 interface 类似,并且实现了结构体的方法。像 Go 一样,好像也遵循了基于继承上架构的原则。

管线|>操作符

我,去!虽然遵循了 unix 管线的设计精髓,但是 Elixir 直接将其加入了语言特性,并且在可读性上做了很多的改动。所有 |> 操作符左侧的数值都是右侧函数的第一个参数。

像这种情况:

toString(multiplyBy(fetchANumber(), 2))

可以写成:

fetchANumber |> multiplyBy(2) |> toString

将操作顺序改成了自然的方式。这同样也是语法糖,更确切的说是你也可以自己创建的

宏也是语法糖,宏系统不仅是简单的模板,Elixir 在内部表现为 lisp 式的三元素元组{函数,元数据,参数},你可以在编译时访问你的抽象语法树。

iex> quote do: sum(1, 2, 3)  {:sum, [], [1, 2, 3]}  iex> quote do: 2 + 3  {:+, [context: Elixir, import: Kernel], [2, 3]}  iex> quote do: IO.puts "hello"  {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["hello"]}

这让 Elixir 非常适合元编程和成为很棒的 DSL。你可以任意在编程语言上扩展你认为缺失的东西,Elixir 并不鼓励滥用宏,只有当你遇到一系列新的问题时再用,抱着“我这样试试...”来驱动你的编程(WIICJDD)的就有点偏离轨道了。

标准库与 OTP

Erlang 的标准库内容居多,采用 PHP 的风格,而 Elixir 则不同,其更为整洁。但是最牛的是它的开放电信平台(OTP),这个库就是针对电信行业的分布式系统设计的。

OTP 是由设计精良的各种模块组成的用于设计弹性分布式系统的库,这也让 Erlang 在某种程度上显得十分特别。在库里面你可以找到前文提及的 supervisor,发布管理,监控,一般的服务器实现,分布式的键值存储甚至还有分布式的关系型数据库。

Phoenix

Phoenix 由另一个 Ruby 使用者 Chris McCord 创建,它是一个高效的 web 框架,目的是成为 Elixir 的 Rails。它充分利用了 Plug 作为其核心的思想,Plug 定义了中间件的实现标准,并且使用 Ecto 作为数据库 ORM 层。它试图借鉴 Rails 专注与生产的特点,但是又不是 Rails 的克隆,而是充分利用了 Elixir/OTP 的特性。它不局限于 web 开发,还内建了一个可以在传输层加入插件的一个 socket 库 Phoenix Channels,并且已经有 JavaScript, Swift, ObjC, C# 和 Java的实现了。

BEAM

前面提到了,BEAM 是 Elixir运行的虚拟机。不要被虚拟机给吓到了,它 与JVM 很不一样。它占用极少的内存,没有显著的内存回收停顿,性能很好。它的出色之处在于它的低延迟,计算速度介于 JAVA/Go 与 Python/Ruby 之间,并且可以在树莓派上欢乐的玩耍。不要愁部署,OTP 提供了包含所有依赖库的“发布助手”,像 Go 的二进制文件一样,你只需 scp 到你的服务器即可。

最后的一些看法

私以为 Elixir 是很有趣的,对于像我这样在 JavaScript 上面找不到太多函数编程思想的人,学习它的范式会感到很自然。模式匹配和管线操作也让代码的书写和阅读更加明了。同时,在学习构建弹性分布式系统这门课程的多年之后,也能够有机会学习一下OTP库。

我看到在为了性能提升的道路上,很多人从 Ruby 转向了 Go,也有人转向了 Elixir。一直有人说,我没有看到它应用在某个大型应用上,最起码在禁止使用宏定义的版本上。我曾写过关于 Go,说过不让用户表达自己的好处。Elixir 和 WIICJDD 的思想则是另外一种景象,通过宏系统,你可以尽情的释放自己。

我想把 José Valim 比喻成 Willy Wonka,他将 Erlang 转换成了很多语法糖,给大家带来了胃口,我把宏比喻成巧克力工厂,我也在考虑有多少开发者会不顾提醒而跳入巧克力河中。我不说更多了,你可以看我的选择,有可能我是错的,毕竟这门语言还太年轻。

想要更多的了解 Elixir 的设计目标,需要看看它背后的人们,以及他们曾经遇到的问题。据我所知,Valim 和 McCord 都不是 Erlang 的开发者,也不想做一个更好的 Erlang,他们都是在 web 开发机构用 Ruby 写代码的[see notes],他们都是想用 Erlang 的分布式的优点来弥补 Ruby可 扩展性的短板[see notes]。他们带领了一些 Erlang 开发者。这门语言立意深远,按照这个思路发展才是最重要的。

如果你被吸引了,想学学它,不仅有官方的入门指导,还推荐你看看 Chris McCord 的 3 个小时的研讨班:都来搭 Elixir 的列车!相关代码

记录-1: 十月 17, 2015

我得知,José Valim 的雇主 Plataformatec 公司正在支持 Elixir 的开发,不仅在 web 开发,还在他们生意中的后端部分也有涉及,比如 SOA,ETL 等地方。

记录-2: 十月 17, 2015

我提及的作者想要把 Elixir 打造成一个更好的 Ruby 只是我个人的想法,这是 Valim 本人的说法:

 我不会将Elixir定义为更好的 Ruby。在 Elixir 之前,我本人的主要语言确实是 Ruby,但是我创建 Elixir 的部分工作/研究的本意是想让其获取更多经验,从而丰富其生态系统。所以我对 Elixir 并没有偏见,不是更好的 Ruby,也不是更好的 Erlang,它就是它自己。

发表于十月 16, 2015