神奇的仙丹,性感的Elixir

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

在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轻松地创建进程:

  1. spawn fn -> 1 + 2 end 

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

  1. send self, {:hello, "world"}receive do 
  2. {:hello, msg} -> msg 
  3. {:world, msg} -> "won't match" 
  4. 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代码:

  1. defmodule KV.Supervisor do 
  2.   use Supervisor 
  3.  
  4.   def start_link do 
  5.     Supervisor.start_link(__MODULE__, :ok) 
  6.   end 
  7.  
  8.   def init(:ok) do 
  9.     children = [ 
  10.         worker(KV.Registry, [KV.Registry]), 
  11.         supervisor(KV.Bucket.Supervisor, []) 
  12.     ] 
  13.  
  14.     supervise(children, strategy: :rest_for_one) 
  15.   end 
  16. end 

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

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

模式匹配

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

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

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

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

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

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

  1. defmodule Factorial do 
  2.     def of(0), do: 1 
  3.     def of(n), do: n * of(n-1)  
  4. end 

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

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

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

管道运算符

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

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

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

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

  1. signature = conn   
  2.             |> get_req_header("x-twilio-signature"
  3.             |> List.first 
  4. if conn   
  5.    |> url_from_conn 
  6.    |> Validator.validate(conn.params, signature) 
  7. do   
  8.   conn 
  9. else   
  10.   conn |> send_resp(401, "Not authorized") |> halt 
  11. end 

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

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

  1. signature = conn   
  2.             |> get_req_header("x-twilio-signature"
  3.             |> List.first 
  4. conn   
  5. |> url_from_conn 
  6. |> Validator.validate(conn.params, signature) 
  7. |> 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包的依赖:

  1. defp deps do 
  2.    [ 
  3.      {:httpoison, "~> 0.11.0"}, 
  4.      {:jsx, "~> 2.8"
  5.    ] 
  6.  end 

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

环境配置

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

  1. import_config "#{Mix.env}.exs" 

单元测试

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

  1. defmodule MyprojectTest do 
  2.   use ExUnit.Case 
  3.   doctest Myproject 
  4.  
  5.   test "sort ascending orders the correct way" do 
  6.     result = sort_into_ascending_order(fake_created_at_list(["c""a""b"])) 
  7.     issues = for issue <- result, do: issue["created_at"
  8.     assert issues == ~w{a b c} 
  9.   end 
  10. end 

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

其他

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

  1. defmodule Sample.App do 
  2.   import Ecto.Query 
  3.   alias Sample.Weather 
  4.   alias Sample.Repo 
  5.  
  6.   def keyword_query do 
  7.     query = from w in Weather, 
  8.          where: w.prcp > 0 or is_nil(w.prcp), 
  9.          select: w 
  10.     Repo.all(query) 
  11.   end 
  12.  
  13.   def pipe_query do 
  14.     Weather 
  15.     |> where(city: "Kraków"
  16.     |> order_by(:temp_lo) 
  17.     |> limit(10) 
  18.     |> Repo.all 
  19.   end 
  20. end 

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

【本文为51CTO专栏作者“张逸”原创稿件,转载请联系原作者】

戳这里,看该作者更多好文

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2015-11-02 17:25:23

Elixir编程语言未来

2013-06-09 10:51:35

2012-08-31 09:53:44

Noa TishbyMinit

2011-06-01 09:49:55

程序员IT

2023-04-18 08:14:27

ElixirRustWebRTC

2012-08-13 09:25:13

Windows 8操作系统

2020-06-08 07:52:31

Python开发工具

2012-08-24 10:49:51

备份恢复

2009-08-27 17:47:21

c#皮肤

2013-05-17 09:00:07

云计算云应用云安全

2020-08-29 19:29:09

Pythonturtle

2012-10-09 13:41:09

数据科学家职业

2017-04-05 11:10:23

Javascript代码前端

2020-10-26 19:19:09

缓存Redis场景

2023-10-13 13:19:02

Java枚举

2013-11-05 09:26:19

Mavericks系统OS X

2009-11-06 18:56:22

Windows 7任务栏

2021-10-08 21:00:52

数据弱引用对象

2018-05-02 16:23:24

中间件RPC容器

2016-08-24 15:39:46

ownCloud存储服务器
点赞
收藏

51CTO技术栈公众号