神奇的仙丹,性感的Elixir

在IT世界里,没有银弹,但却有神奇的仙丹(Elixir)。我不知道是什么灵感刺激这门语言的创造者José Valim想到了这么酷的命名,但这枚仙丹确实经由多种神奇的灵药炼制而成,这些灵药包括Erlang、Ruby、Clojure、Haskell。

品尝这枚仙丹确实令人飘飘欲仙,至少,我在浅尝Elixir时,这种奇妙的感觉一直萦绕在我心间,怦然心动因而不舍离去。或许如Erlang之父Joe Armstrong所说,是“一种先行于逻辑的内心感性的感觉”;又或者如Dave Thomas形容的,那是让人“坠入爱河”的感觉。

大爱Elixir。

我之所以爱上Elixir,大约还是因为Ruby的缘故。我并非Ruby的狂热追随者,甚至没有从事太多Ruby相关的项目,但我至今在编写脚本时,Ruby依旧是我的首选。在动态语言中,我甚喜爱Ruby相对简洁的语法。当我看到Elixir时,那种似曾相识的感觉让我心动。

虽然说Elixir的炼制来自各位前辈留下的丹方灵药,然而从成丹之日起,Elixir就是Elixir,她已经具有了完整的语言性格。就我看来,Elixir真正称得上是“性感”。当然,这一大半要归功于Erlang美丽的英伦风情(Erlang之父Joe Armstrong是英国人),就Erlang的高颜值打底,只需再加上几点妩媚,几分妖娆,风采就变得性感撩人了。

并发与分布式

Elixir对并发与分布式的支持,就是正宗的英伦风情,这是从Erlang延续下来的最强悍基因。Elixir建立在Erlang虚拟机(BEAM)之上,使用Erlang的进程,如原生进程那样在所有的处理器中运行,然而开销却非常小。与Erlang一样,Elixir可以通过spawn轻松地创建进程:

spawn fn -> 1 + 2 end

Elixir或者说Erlang的进程依靠消息传递完成通信。进程接收到的消息实际上是获取的一份消息副本,这就使得接收方能够与发送方解耦,接收方对消息的任何操作不会影响接收方。

send self, {:hello, "world"}

receive do
  {:hello, msg} -> msg
  {:world, msg} -> "won't match"
end

Elixir的核心继承自Erlang,自然就继承了对OTP(Open Telecom Platform)的支持。OTP是一个很大的课题,包括进程链接、监控以及分布式支持(我正在学习《Erlang/OTP并发编程实战》,希望从Erlang根源上理解OTP)。Elixir对OTP的支持包括Agent、Task、GenServer以及Supervisor与Application。其中,Agent与Task是Elixir对OTP特性的抽象,而GenServer则更加通用。

在Elixir创建OTP服务器非常简单,只需要use GenServer即可。它主要的方法为handle_call(request, from, state)与handle_cast(request, state)。如果客户端发送的请求需要响应时,则消息形式为call,如果为单向调用,则形式为cast。

考虑进程的健壮性问题,在编写OTP应用时,可能还需要对进程进行监督。基于Actor模型,父进程将负责监督由其创建的所有子进程,下面的代码是Elixir官方提供的Supervisor代码:

defmodule KV.Supervisor do
  use Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, :ok)
  end

  def init(:ok) do
    children = [
        worker(KV.Registry, [KV.Registry]),
        supervisor(KV.Bucket.Supervisor, [])
    ]

    supervise(children, strategy: :rest_for_one)
  end
end

KV.Supervisor为监督进程,其子进程分别为KV.Registry与KV.Bucket.Supervisor,监督策略为rest_for_one。

至于分布式支持,在Elixir其实是水到渠成的事情,因为它的核心是进程间通信,而进程所在的节点位置,对于用户而言是透明的。

模式匹配

模式匹配是Elixir最妖娆的部分,虽然很多函数式语言都有模式匹配,但Elixir却把模式匹配融入到其血肉之中(其实是延续了Erlang的模式匹配特色)。即使是一个赋值语句,也是模式匹配的一部分。在Elixir中,=符号其实被称之为匹配运算符(match operator)。所以你可以写出违反程序员常规的1 = x:

iex> x = 1
1
iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1

模式匹配在Elixir中被广泛地运用到解构(destructuring )复杂的数据结构,例如Tuple、List等。当然case进行的模式匹配更是它最常见的使用场景。

函数与模式匹配的结合才是体现妖娆性的关键点,如果再结合guard clause,那就真正让人销魂了。

大多数语言的函数定义是支持函数重载的,这取决于参数的类型、个数与顺序。在动态语言中没有类型,则与个数与顺序相关。这些参数在定义时皆为形参(部分语言支持默认参数值,Elixir也支持,甚至可以将表达式作为默认参数),在调用时才传入实参。

但是,Elixir则不然,因为Elixir没有赋值的概念,因此在传递参数时,并非赋值的语义,而是匹配的语义。因而出现如下的函数定义,你不要感到诧异哦:

defmodule Factorial do
    def of(0), do: 1
    def of(n), do: n * of(n-1)
end

在Elixir语义中,这两个定义实则是同一个函数,当调用of函数时,传入的参数会与第一个定义进行匹配,如果匹配不成功,则匹配第二个定义。利用这种模式匹配,既可以规避实现上的if分支,又可以更好地体现递归的语义。

Meyer非常强调软件开发中对“契约”的遵循,在他设计的语言Eiffel中,前置条件与后置条件作为了语法糖中的一等公民被支持。Erlang的guard Cluase与Eiffel的前置条件非常相似,Elixir也保留了这一语法特性。例如在前面的阶乘算法中,我们可以通过guard clause避免传入错误的负数:

defmodule Factorial do
    def of(0), do: 1
    def of(n) when n > 0, do: n * of(n-1)
end

管道运算符

让Elixir展现其妩媚一面的,是超级性感的管道运算符。她让整段代码瞬间变得可爱起来。有了她,我们就不用再陷入可怕的函数嵌套地狱中了。Dave Long在博文Playing with Elixir Pipes中给出了一个颇有对照意义的例子。代码功能是从conn取得Request的header,并判断它是否有效。如果有效就返回conn,否则终止,并返回Not Authorized。

如果没有管道运算符,就得承受嵌套函数调用的惊悚感:

signature = List.first(get_req_header(conn, "x-twilio-signature"))  
is_valid = Validator.validate(url_from_conn(conn), conn.params, signature)  
if is_valid do  
  conn
else  
  halt(send_resp(conn, 401, "Not authorized"))
end

这样的代码完全违反人类直觉,因为你得从函数最里边阅读,然后再层层往外逃逸。是否有一种被紧紧捆绑了的感觉呢?当然,在很多语言中我们都无奈地接受了这一点,已经被虐得习以为常了。尝试一下管道运算符,会怎么样?

signature = conn  
            |> get_req_header("x-twilio-signature")
            |> List.first
if conn  
   |> url_from_conn
   |> Validator.validate(conn.params, signature)
do  
  conn
else  
  conn |> send_resp(401, "Not authorized") |> halt
end  

当你把管道运算符|>看成是goto的话,我们就能直观地体会到conn在各个函数中流动的现象了。非常可爱,不是吗?

Elixir是纯正的函数式语言,本质上讲,Elixir中的一切皆为函数,所以if表达式其实也是函数。这就意味着validate后的布尔结果可以通过|>直接传递给if:

signature = conn  
            |> get_req_header("x-twilio-signature")
            |> List.first
conn  
|> url_from_conn
|> Validator.validate(conn.params, signature)
|> if(do: conn, else: conn |> send_resp(401, "Not authorized") |> halt)

这才是真正Elixir Style的编程范儿,够妩媚吧!

Joe Armstrong认为管道运算符来自Prolog语言的隐性基因DCG,类似Haskell中的monad。Prolog的儿子erlang没有体现这一点,孙子辈又隔代遗传上了。

工程支持

Elixir的创造者José Valim乃Rails的核心参与者,所以他把Rails社区(包括Ruby社区)中一套让人目眩的工程实践照般过来了。

脚手架

通过mix可以直接帮助我们创建项目的脚手架(用过rails的童鞋感到亲切了吗?):

mix new myproject

执行这条命令,mix就会帮我们创建项目的基本结构和相应文件:

包管理与依赖管理

通过Hex来管理包(记得GEM吗?)。在http://hex.pm中几乎可以找到所有你想要的elixir包;当然你还可以享受Erlang的福利,直接重用erlang包。

添加依赖也非常方便,只需要在项目的mix.exs文件中添加依赖即可。例如添加HTTPoison和JSX包的依赖:

  defp deps do
    [
      {:httpoison, "~> 0.11.0"},
      {:jsx, "~> 2.8"}
    ]
  end

最棒的是,Elixir还支持直接对github repository的依赖。

环境配置

对开发环境、测试环境、生产环境的配置支持。在config目录下的config.exs文件中可以添加必要的配置项,还可以通过如下语句import不同环境的配置:

import_config "#{Mix.env}.exs"
单元测试

还有不能忘记的单元测试,这可是敏捷社区的随身法宝啊;Elixir通过内嵌的ExUnit很好地支持了单元测试的编写:

defmodule MyprojectTest do
  use ExUnit.Case
  doctest Myproject

  test "sort ascending orders the correct way" do
    result = sort_into_ascending_order(fake_created_at_list(["c", "a", "b"]))
    issues = for issue <- result, do: issue["created_at"]
    assert issues == ~w{a b c}
  end
end

如此简单。要运行所有测试,只需运行mix test即可。

其他

Elixir还有很多酷炫的玩意儿,例如Protocol、Behavior,当然还有最棒的(当然也可能是最令人费解的)宏(Macro)。Elixir对DSL的支持也非常友好,这来自它继承的部分Ruby血统。例如,让我们看看ECTO(一个基于Elixir开发的支持数据库访问的框架)的一段客户代码:

defmodule Sample.App do
  import Ecto.Query
  alias Sample.Weather
  alias Sample.Repo

  def keyword_query do
    query = from w in Weather,
         where: w.prcp > 0 or is_nil(w.prcp),
         select: w
    Repo.all(query)
  end

  def pipe_query do
    Weather
    |> where(city: "Kraków")
    |> order_by(:temp_lo)
    |> limit(10)
    |> Repo.all
  end
end

因为没有大括号、括号以及分号的干扰,代码可以变得更接近领域逻辑,再加上性感的管道运算符,可读性直接爆表,帅呆了!

2017-02-09 10:33870FPErlangElixir