为什么选择Elixir

我开始使用 Elixir 大约有一年了。起初,我只打算在博客中使用这种语言,认为它可以帮助我更好地说明 Erlang 虚拟机(EVM)的优势。然而,我立即被这种语言所带来的魅力所吸引,并很快将其引入了我当时正在开发的基于 Erlang 的生产系统。如今,我认为 Elixir 是开发 EVM 支持系统的更好选择,在这篇文章中,我将尝试强调它的一些优点,并消除对它的一些误解。

Erlang 语言的问题

EVM有很多优点,可以让我们更轻松地构建高可用、可扩展、容错的分布式系统。互联网上有各种证明,我在博客中也提到了 Erlang 的一些优点,在这里这里,我即将出版的新书《Elixir in Action》的第一章介绍了 Erlang 和 Elixir 的优点。

长话短说,Erlang 为管理高度可扩展的容错系统提供了出色的抽象,这在必须执行许多独立或松散依赖任务的并发系统中尤其有用。三年多来,我一直在生产中使用 Erlang,构建了一个基于长轮询的 HTTP 推送服务器,在高峰期每秒可提供超过 2000 次请求(非缓存)。在此之前,我从未编写过如此大规模的程序,也从未开发过如此稳定的程序。这项服务就这样快乐地运行着,无需我多想。这实际上是我的第一份 Erlang 代码,其中充满了反模式和糟糕的方法。但事实证明,EVM 仍然具有很强的适应能力,并能以最好的状态运行代码。最重要的是,由于 Erlang 的并发机制,我可以非常直接地解决这个复杂的问题。

不过,尽管 Erlang 有一些很好的特性,但我在用 Erlang 编程时却从来没有(现在也没有)感到十分自如。不知何故,编码过程总是感觉不太流畅,而且生成的代码总是带有过多的模板和重复。问题不在于语言的语法。我在学生时代学过一点 Prolog,我非常喜欢这种语言。推而广之,我也喜欢 Erlang 的语法,而且实际上我认为它在很多方面都比 Elixir 更好、更优雅。我是一名面向对象(OO)开发人员,大部分时间都在使用 Ruby、JavaScript、C# 和 C++ 等语言。

Erlang 给我带来的问题是,这种语言在某种程度上过于简单,很难消除模板和结构上的重复。相反,由此产生的代码也会变得有点乱,更难编写、分析和修改。在使用 Erlang 编程一段时间后,我认为就高效代码组织而言,函数式编程不如面向对象编程。

Elixir 是什么(不是)

这就是 Elixir 改变我看法的地方。在我花了足够多的时间使用 Elixir 之后,我终于能够更清楚地看到函数式编程的好处和优雅之处。现在,我不能再说我更喜欢 OO 而不是 FP 了。我发现在 Elixir 中的编码体验要愉悦得多,我可以专注于我要解决的问题,而不是处理语言的缺点。

在讨论 Elixir 的一些优点之前,我想强调一件重要的事情:Elixir 不是 Erlang 的 Ruby。它也不是用于 Erlang 的 CoffeeScript、Clojure、C++ 或其他语言。Elixir 和 Erlang 之间的关系是独特的,Elixir 在语义上往往与 Erlang 非常接近,但又从不同的语言中引入了许多想法。从表面上看,Elixir 的最终结果可能很像 Ruby,但我觉得它更接近 Erlang,两种语言完全共享类型系统,走的是相同的函数式路线。

那么 Elixir 是什么呢?在我看来,Elixir 是一种类似 Erlang 的语言,具有更好的代码组织能力。这个定义与你在官方页面上看到的不同,但我认为它抓住了 Elixir 与 Erlang 相比的精髓。

让我详细说明一下。在我看来,编程语言有以下几个作用:

  • 它作为一种界面,允许程序员控制某些东西,例如硬件、虚拟机、运行中的应用程序、用户界面布局......
  • 它塑造了开发人员思考他们正在建模的世界的方式。OO 语言会让我们寻找具有状态和行为的实体,而 FP 语言则会让我们考虑数据和转换。声明式编程语言会迫使我们思考规则,而在命令式语言中,我们会更多地考虑动作序列。
  • 它提供了组织代码、去除重复、模板和噪音的工具,并希望尽可能按照我们理解的方式对问题进行建模。

Erlang 和 Elixir 在前两个角色上完全相同--它们的目标是相同的 "事物"(EVM),并且都采用了函数式方法。Elixir 在第三种作用上对 Erlang 进行了改进,为我们提供了额外的工具来组织代码,并希望能更高效地编写生产就绪、可维护的代码。

构成要素

互联网上关于 Elixir 的讨论很多,但我尤其喜欢 Devin Torres 的两篇文章,你可以在这里这里找到。Devin 是一位经验丰富的 Erlang 开发人员,他还编写了一个广受欢迎的 poolboy 库,因此他对 Elixir 的看法值得一读。

我将尽量不重复很多内容,并避免涉及许多机械细节。相反,让我们简要介绍一下可以用来更好地组织代码的主要工具。

元编程

Elixir 中的元编程有几种形式,但本质是一样的。它允许我们编写简洁的结构体,这些结构体仿佛是语言的一部分。这些结构在编译时会被转换成合适的代码。在机械层面上,它可以帮助我们消除结构上的重复--即两段代码具有相同的抽象模式,但在许多机械细节上存在差异。

例如,下面的代码段展示了一个用户记录模型模块的简图:

defmodule User do
  #initializer
  def new(data) do ... end

# getters
def name(user) do ... end
def age(user) do ... end

# setters
def name(value, user) do ... end
def age(value, user) do ... end
end

其他类型的记录也会遵循这种模式,但包含不同的字段。我们可以使用 Elixir defrecord 宏来代替复制粘贴这种模式:

defrecord User, name: nil, age: 0

根据给定的定义,defrecord 会生成一个专用模块,其中包含用于操作用户记录的实用功能。因此,通用模式只在一个地方(defrecord 宏的代码)进行了说明,而特定的逻辑则不需要机械地执行细节。

Elixir 宏与 C/C++ 宏完全不同。它们不处理字符串,而是类似于编译时的 Elixir 函数,在解析过程中被调用,并处理抽象语法树(AST),即以 Elixir 数据结构表示的代码。宏可以在 AST 上工作,并产出一些替代的 AST 来表示生成的代码。因此,宏是在编译时执行的,所以一旦进入运行时,性能就不会受到影响,也不会出现某些代码会更改模块定义的意外情况(例如 JavaScript 或 Ruby 中就会出现这种情况)。

由于宏的存在,Elixir 的大部分功能实际上都是在 Elixir 中实现的,包括 if、unless 或单元测试支持等构造。Unicode 支持的工作原理是读取 UnicodeData.txt 文件,并生成相应的 Unicode 字符串函数(如downcase或upcase)。这反过来又使开发人员更容易为 Elixir 做出贡献。

宏还允许第三方库作者提供与语言自然匹配的内部 DSL。Ecto 项目提供嵌入式集成查询,类似于 Elixir 的 LINQ,这是我个人最喜欢的项目,它真正展示了宏的力量。

有时,我看到有人对 Elixir 不屑一顾,说他们不需要元编程功能。元编程虽然非常有用,但也可能成为非常危险的工具,建议谨慎使用。尽管如此,元编程还是提供了很多功能,即使你不亲自编写宏,你也可能会喜欢其中的很多功能,比如前面提到的记录、Unicode 支持或集成查询语言。

管道运算符

这个看似简单的运算符非常有用,甚至在我知道 Elixir(或其他语言)中存在这个运算符之前,我就 "发明 "了它的 Erlang 对应运算符。

让我们先来看看问题所在。Erlang 中没有管道运算符,此外,我们也无法重新分配变量。因此,典型的 Erlang 代码通常会以如下模式编写:

State1 = trans_1(State),
State2 = trans_2(State1),
State3 = trans_3(State2),
...

这是一段非常笨拙的代码,它依赖于中间变量,以及将上一次结果正确传递给下一次调用。实际上,我曾遇到过一个令人讨厌的错误,因为有一处我不小心使用了 State6 而不是 State7。

当然,我们可以通过内联函数调用来解决这个问题:

trans_3(
  trans_2(
    trans_1(State)
  )
)

正如你所看到的,这段代码很快就会变得很难看,而且当转换函数接收到额外参数和转换次数增加时,问题往往会更加严重。

管道运算符可以在不使用中间变量的情况下组合各种操作:

state
|> trans_1
|> trans_2
|> trans_3

这段代码从上到下读起来就像散文一样,突出了 FP 的优势之一,即我们将函数视为数据转换器,通过各种方式将其组合起来,以实现所需的结果。

例如,下面的代码计算一个列表中所有正数的平方和:

list
|> Enum.filter(&(&1 > 0))       # take positive numbers
|> Enum.map(&(&1 * &1))         # square each one
|> Enum.reduce(0, &(&1 + &2))   # calculate sum

由于 Elixir 库中的 API 遵循 "主语(名词)作为第一个参数 "的惯例,因此管道操作符运行得非常好。与 Erlang 不同,Elixir 认为所有函数都应将其操作对象作为第一个参数。因此,String 模块函数将 string 作为第一个参数,而 Enum 模块函数则将 enumerable 作为第一个参数。

通过协议实现多态性

协议是 Elixir 提供大致类似于 OO 接口的方式。起初,我对协议的印象并不深刻,但随着时间的推移,我开始发现协议带来的诸多好处。协议允许开发人员创建一个通用逻辑,该逻辑可用于任何类型的数据,前提是为给定的数据实现了某种契约。

枚举模块就是一个很好的例子,它提供了许多有用的函数,可用于处理任何可枚举的数据。例如,我们可以这样遍历一个枚举:

Enum.each(enumerable, fn -> ... end)

当然,我们也可以通过实现相应的协议为自己的类型添加支持。这类似于 OO 接口,但有一个额外的特点,即即使你不拥有某个类型的源代码,也可以为该类型实现协议。

协议实用性的最佳例子之一是流模块,它实现了一种lazy的、可组合的、可枚举的抽象。通过流,可以组合各种可枚举的转换,然后只在需要时,通过将流输入 Enum 模块中的某个函数来生成结果。例如,下面的代码可以一次性计算出列表中所有正数的平方和:

list
|> Stream.filter(&(&1 > 0))
|> Stream.map(&(&1 * &1))
|> Enum.reduce(0, &(&1 + &2))   # Entire iteration happens here in a single pass

在第 2 行和第 3 行中,操作已经组成,但尚未执行。结果是一个实现 Enumerable 协议的规范描述符。一旦我们将该描述符输入某个枚举函数(第 3 行),它就会开始产生值。除了支持协议机制外,Elixir 编译器没有提供特殊的lazy支持。

Mix工具

最后一块重要的拼图是帮助我们管理项目的工具。Elixir 附带的Mix工具就能做到这一点。它的操作方式也非常简单,令人印象深刻。创建一个新项目时,磁盘上只会创建 7 个文件(包括 .gitignore 和 README.md)。这就是创建一个合适的 OTP 应用程序所需的全部过程。这是一个很好的例子,说明通过将必要的模板和官僚主义隐藏在通用抽象中,可以将事情简化到何种程度。

Mix工具还支持其他各种任务,如依赖关系管理。该工具还具有可扩展性,因此您可以根据需要创建自己的特定任务。

语法变化

Elixir 还为我们带来了许多其他好处。其中许多语法变化都来自 Erlang,例如支持变量重绑定、可选括号、隐式语句结尾、可归零性、短路操作符......

诚然,可选的括号会带来一些歧义,如本示例所示:

abs -1 + 5    # same as abs(-1 + 5)

不过,我使用括号(宏和零 arg 函数除外),所以我不记得在实践中遇到过这个问题。

总的来说,我喜欢这里做出的许多决定。能写 if 而不强制写 else,这很好。我也很高兴不必有意识地考虑必须使用哪个字符来结束语句。

即使是可选的括号也很好,因为它们支持宏的 DSL 式使用,使代码不那么嘈杂。如果没有括号,我们就必须在调用宏时添加括号:

defrecord User, name: nil, age: 0       # without parentheses

defrecord(User, [name: nil, age: 0])    # with parentheses

不过,我并不觉得这些增强功能有多重要。它们是很好的点睛之笔,但如果 Elixir 只能提供这些,我可能还是会使用纯 Erlang。

结束语

这篇文章已经说了很多,但我觉得 Elixir 的魔力还远未被完全捕捉。诚然,对语言的偏好是主观的,但我觉得 Elixir 确实在 Erlang 的基础上进行了改进。我用 Erlang 进行了三年多的生产级编码,而使用 Elixir 则有一年左右的时间。生成的代码看起来更紧凑,我可以更专注于解决我正在解决的问题,而不是与过多的噪音和模板搏斗。

出于类似的原因,我喜欢 EVM。底层的并发机制让我可以更轻松地处理高负载服务器端系统的复杂性,该系统必须不断提供服务并同时执行许多任务。

Elixir 和 EVM 都提高了抽象标准,帮助我更轻松地解决复杂问题。这就是为什么我总是将 Elixir/EVM 组合作为构建服务器端系统的首选工具。当然,具体情况具体分析。

Related Posts

Elixir和Phoenix是2022年Web应用程序的绝佳选择

如何在 2022 年为应用选择最佳的 Web 编程语言和框架?这是可能吗?我相信是的,在这篇博文中,我将尝试说服您为什么 Elixir 和 Phoenix 是您正在寻找的完美组合。 ## Elixir: 生产力等于更少的成本 生产力仍然是编程语言的一个被大大低估的特性。一般来说,市场上的大多数应用程序都必须实现一些业务目标,众所周知,我们产生的成本越少

Read More

为什么选择Elixir

我开始使用 Elixir 大约有一年了。起初,我只打算在博客中使用这种语言,认为它可以帮助我更好地说明 Erlang 虚拟机(EVM)的优势。然而,我立即被这种语言所带来的魅力所吸引,并很快将其引入了我当时正在开发的基于 Erlang 的生产系统。如今,我认为 Elixir 是开发 EVM 支持系统的更好选择,在这篇文章中,我将尝试强调它的一些优点,并消除对

Read More

使用 Erlang 和 Elixir的成功公司

Erlang 是一种受信任且稳定的语言,用于在主要系统中运行核心程序,由 Ericsson 发明。Erlang(和 Elixir)被许多领域的许多行业使用,包括金融科技、安全、区块链和物联网。 公司之所以选择 Erlang 和 Elixir,是因为可以轻松编写可部署在分布式网络中的容错和可扩展程序。Erlang 和 Elixir 都是函数式语言,它们可以使

Read More