Elixir 从入门到放弃

过去将近一年的时间里,作者在日常工作中需要经常与 Elixir 这门编程语言打交道,包括使用 Phoenix 框架开发一些后端服务、实现定制的组件,读过之前 2018 年总结 一文的读者相信对此也有所了解。

elixir-programming-language

在使用 Elixir 的过程中,发现这门编程语言有很多非常有趣的概念和设计,但是也遇到了更多很难甚至无法解决的问题,最终种种原因最终选择使用 Golang 替换掉生产环境中全部的 Elixir 项目,随着作者将主要编程语言逐渐迁移到 Go,觉得还是有必要对 Elixir 比较独特的语言谈一谈作者的经历和看法。

我们将在这篇文章中为各位读者介绍 Elixir 这门编程语言、作者和它相处的体验和经历、最终为什么选择放弃这门编程语言以及不推荐各位工程师在生产环境中使用的原因,需要注意的是,文章开始的一部分会介绍 Elixir,后面会包含大量主观的评价和想法,欢迎有不同意见的读者在下面留言,也希望各位 Elixir 的拥趸能够多多包涵。

概述

Elixir 官方将其定义成『一个用于构建可伸缩、可维护应用的动态函数式编程语言』,它运行在 Erlang 的虚拟机上,能够充分利用虚拟机的优点运行低延时、高容错的分布式系统。

elixir-features

想要通过几个关键字来描述一个编程语言的全部内容是比较困难的事情,我们只能在这有限的篇幅内讨论几个有趣的 Elixir 语言特性 — 函数式、面向并发、动态类型。

函数式

函数式编程其实是一种 编程范式,它强调程序执行的结果而不是过程,我们往往需要通过对函数进行组合以设计执行过程,每一个函数的结果只依赖于函数的参数,除了参数之外的任何数据都不会影响执行结果。

从最开始通过面向过程的 C 语言学习编程,到后来使用各种各样的面向对象语言 Objective-C、Ruby,在工业界,函数式编程语言一直没有被广泛应用,Haskell 这种太过于学术的纯函数式编程语言基本上没有太多市场,而 Elixir 这种编程语言虽然摆脱了『过于学术』的帽子,但是依然是一个小众语言,不过目前很多广泛流行的现代编程语言却通过曲线救国的方式,同时支持包括函数式编程在内的多种 编程范式,所以相信很多工程师对于这一个概念也并不陌生。

作者从学习 计算机程序的构造和解释(SICP) 一书开始,就一直想在生产环境中尝试使用函数式编程语言,体验一下不同的编程风格,所以接受这门编程语言对于作者来说还是非常自然的。

不可变

在学习 Elixir 这门语言中我们其实能够体会到函数式编程中经常被提到的特性 — 数据结构不可变,即所有数据结构一旦被初始化就不能被改变,我们简单举一个例子就能看出这一特性的影响,在常见的面向对象语言中,我们可能会写出如下的代码:

class Counter
    attr_reader :value
    def initialize
        @value = 0
    end

    def incr
        @value += 1
    end
end

counter = Counter.new
counter.incr
puts counter.value # => 1

通过构造器方法 Counter.new 创建一个新的 Counter 对象,这个对象内部持有一个 value 用于保存当前值,每次调用 incr 方法都会修改对象持有的实例变量,也就是说 counter 对象在初始化之后其实被自己的方法 incr 修改了,这个场景在 Elixir 的实现下就完全不同了,想要实现类似的功能我们只能通过以下的方式:

defmodule Counter do
  defstruct [:value]

  def new() do
    %Counter{value: 0}
  end

  def incr(counter) do
    %Counter{value: counter.value + 1}
  end
end

counter = Counter.new()
counter = Counter.incr(counter)
IO.puts counter.value # => 1

上述代码会使用 Counter.new 构建一个新的 Counter 结构体,该结构体与 Ruby 代码中的对象没有太多的不同,但是在 Counter.incr 函数中,Elixir 代码创建了一个全新的结构体并将返回的新结构体赋值回了 counter 变量,这其实就是数据结构不可变带来的影响,我们需要『通过创建新的数据结构来覆盖已有的数据结构』

这种创建新结构的方式带来的额外开销会比直接修改实例变量大得多,但是我们却不用担心多个线程同时修改同一块内存带来的竞争问题,能够更好地支持并发编程。

模式匹配

函数式编程一个比较常见的特性就是模式匹配,虽然有模式匹配的编程语言不一定是函数式编程语言,但是函数式编程语言基本上都可以使用模式匹配,它为编程语言提供了一种解构数据的方法,Elixir 的函数就可以使用模式匹配将同一个函数的不同逻辑根据入参分散到多个更短的函数中:

defmodule Transaction do
  def execute(%Transaction{status: :pending}) do
    # ...
  end

  def execute(%Transaction{status: :completed}) do
    # ...
  end
  
  def execute(%Transaction{}) do
    # fallback
  end
end

与其他编程语言中的 switch/case 相比,模式匹配的功能更加强大,这个特性不仅能够对数据结构进行解构,还能够帮助根据函数的入参对方法逻辑进行拆分使代码更加清晰。

面向并发

Elixir 是面向并发的编程语言,基于 Erlang 的 BEAM 虚拟机,Elixir 能够非常简单地实现并发编程,它的并发模型是 actor 模型,这一节会介绍 Actor 并发模型和 Elixir 中的 OTP 行为。

Actor

Actor 模型 是一种并发计算模型,其中的 actor 指的是计算的基本单位,actor 模型最开始是在 1973 年 Carl Hewitt 的论文 A Universal Modular Actor Formalism for Artificial Intelligence 中被提出的,它包含了三个非常重要的概念:

actor-mode

在 actor 模型中,所有的 actor 都以一个系统的形式存在,因为独立存在的 actor 并不具有任何意义,只有成为一个系统才能发挥它的作用,其实就是:

One ant is no ant and one actor is no actor.

每个 actor 都包含一个用于存储消息的邮箱,它在同一时间只会处理一条消息,如果当前 actor 正在处理消息,那么所有发送给当前 actor 的消息都会被存储到邮箱中等待处理。

所有消息都是以异步的方式发送给其他的 actor 的,当系统中的 actor 接收到来自其他 actor 的消息时,它只可以做以下三件事情中的一件:

  1. 创建更多的 actor;
  2. 向其他 actor 发送消息;
  3. 指定当前的 actor 如何处理下一个消息;

上述三件事情中的最后一件我们可以理解为当前的消息会改变 actor 的状态,更改后的状态将用于处理下一条消息,也就是说发送给当前 actor 的消息会改变当前其存储的状态。

上述视频是 actor 模型的提出者 Hewitt 对其概念的介绍,想要深入了解 actor 模型的读者一定不能错过这个视频。

Elixir 或者说 Erlang 其实就使用了 actor 模型,在 Elixir 中,actor 其实就是进程,这里说的进程和我们在操作系统中的进程并不一样,Elixir 中的进程只是用户态进程,我们可以将其理解成协程或者 Go 语言中的 Goroutine。

当 Elixir 的一个进程由于出现错误而退出时,该进程的监督者(supervisor)就会根据预先配置的选项对该进程进行重启,由于当前进程的退出并不会影响其他进程的正常工作,而且重启后的进程能够恢复对外的服务,所以进程出错或者重启并不会对整个应用的服务质量造成太大的影响,系统中的多个进程相互独立互不影响并且能够在出现异常崩溃时自愈,这也是 Elixir 和 Erlang 的设计哲学 “Let it crash”

OTP

OTP 是谈到 Elixir 和 Erlang 时一定会提到的话题,在 Google 上搜索 Elixir 加上 OTP 会出现非常多的结果,这篇文章也不能免俗,我们还是要说一说 OTP 是什么东西。OTP 的全称是 Open Telecom Platform,即开放电信平台,虽然这里要介绍的东西与电子通信没有太多的关系,但是这却是 Elixir 中非常重要的概念。

Elixir 中的 OTP 其实就是一个包,它包含一系列的行为,包括 GenServerApplicationSupervisor,这里我们从通用服务器 GenServer 出发对 Elixir 面向并发的开发模式和 OTP 本身进行介绍。

GenServer 作为一个行为,它包含了一些的需要实现的函数,在运行时是一个 Elixir 中的进程,也是 actor,我们可以将它理解成一个 Java 中的 interface,如果我『实现』了一个名为 GenServer 的接口,就需要实现一些方法,我们可以先看一下 Elixir 中 GenServer 行为定义的一些函数签名:

init(init_arg)
handle_call(request, from, state)
handle_cast(request, state)
handle_info(request, state)

首先是用于初始化服务器的 init 函数,该函数会返回一个服务器的初始状态,之后的三个函数就是通用服务器 GenServer 用于处理不同服务器消息时使用的回调,handle_call 处理同步调用,handle_cast 处理异步调用,最后的 handle_info 处理向当前进程发送的通用消息。

elixir-genserve

我们使用 GenServer 官方文档中的 例子 来介绍应该如何使用 GenServer,这是一个用 GenServer 实现的栈:

defmodule Stack do
  use GenServer

  def init(stack) do
    {:ok, stack}
  end

  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end

  def handle_cast({:push, item}, state) do
    {:noreply, [item | state]}
  end
end

{:ok, pid} = GenServer.start_link(Stack, [:hello])

GenServer.call(pid, :pop)            #=> :hello
GenServer.cast(pid, {:push, :world}) #=> :ok
GenServer.call(pid, :pop)            #=> :world
  1. 首先调用 GenServer.start_link/2 函数,最终会执行 Stark.init/1 函数,这个函数对栈的内容进行了初始化,初始化后的栈包含了 :hello 这个符号,函数会返回一个新的栈进程 pid,我们可以将这个 pid 理解成当前 GenServer 的地址;
  2. 通过 GenServer.call/2 函数向栈进程发送 :pop 同步消息,这时栈会运行 handle_call 回调,从栈中取出最上层的值 :hello 返回;
  3. 调用 GenServer.cast/2 函数向栈进程发送 {:push, :world} 消息,这个调用因为是异步的并且不会判断目标进程是否存在,而是会直接返回 :ok,栈进程会使用 handle_cast 回调处理当前的消息将 :world 入栈;
  4. 最后的 GenServer.call/2 函数调用其实跟之前的没有什么区别,只是这时栈的内容不同了,所以会返回刚刚入栈的 :world

GenServer 就是包含了一组通用服务器需要实现的回调,每一个 GenServer 的进程其实是一个 actor,这些不同的进程之间互不影响,只会通过发送消息的方式进行通信,正如我们上面提到的 one actor is no actor,Elixir 由 GenServer 和其他进程构成的树状结构具有非常强大的生命力。

elixir-process-tree

进程树中的监督者 Supervisor 会在它管理的 GenServer 进程出现错误时进行处理,一般情况下挂掉的 GenServer 都会被 Supervisor 重启,能够提供非常良好的『自愈』能力。

动态类型

作为动态类型的编程语言,Elixir 中的类型都是在运行时才被推断出来的,当然我们也可以使用类型规格(typespec)在编译期间声明函数的签名和自定义的类型,使用类型规格声明函数之后,Erlang 的工具就会对源代码进行静态的类型检查,提前暴露出代码中类型不一致的问题,但是类型规格在运行时也没有任何的作用,我们可以将其理解成辅助编译器工作的编辑器指令。

函数定义

虽然 Elixir 是动态类型的语言,但是对于基础类型来说,它还是会进行静态类型检查,只是在函数的调用上并没有太多的限制,我们如果想要为函数引入静态类型检查,就需要使用如下所示的类型规格:

@spec and(boolean, boolean) :: boolean
def and(false, _), do: false
def and(_, false), do: false
def and(true, true), do: true

然后在项目中引入 dialyzer 或者 dialyxir 工具,这些工具就能够帮助 Elixir 和 Erlang 实现静态类型检查,在编译期间就能发现代码中的类型错误。

行为

Elixir 中的行为与 Java 中的接口有些相似,它能够保证一个模块实现了一系列的函数,如果当前模块没有实现该行为就会出现错误,上面提到的 GenServer 其实就是一个行为:

defmodule GenServer do
  @callback init(init_arg :: term) ::
              {:ok, state}
              | {:ok, state, timeout | :hibernate | {:continue, term}}
              | :ignore
              | {:stop, reason :: any}
            when state: any

  @callback handle_call(request :: term, from, state :: term) ::
              {:reply, reply, new_state}
              | {:reply, reply, new_state, timeout | :hibernate | {:continue, term}}
              | {:noreply, new_state}
              | {:noreply, new_state, timeout | :hibernate | {:continue, term}}
              | {:stop, reason, reply, new_state}
              | {:stop, reason, new_state}
            when reply: term, new_state: term, reason: term
  # ...
end

如果一个模块想要实现 GenServer 行为,就需要实现所有使用 @callback 定义的函数,有的读者可能会好奇 — 为什么我们在使用 GenServer 时没有定义这些函数却依然通过了编译,这是因为 use GenServer 语句为这些方法提供了默认的实现。

入门

作者刚刚听说 Elixir 这门语言其实是比较早的,大二的时候就有学长曾经提过这门编程语言,因为之前一直用 Rails 来开发服务端项目,后来经常看到有人使用 Elixir 替代 Ruby 开发后端服务,就入坑了 Elixir 的坑。

语法友好

刚刚开始学习和体验 Elixir 还是因为它与 Ruby 有一些比较相似的语法和结构,相信很多 Elixir 的开发者之前也都有 Ruby 和 Rails 的经验,我们还是回到上面用 Ruby 实现计数器的一个代码片段:

class Counter
  attr_reader :value
  
  def initialize
    @value = 0
  end
  
  def incr
    @value += 1
  end
end

使用 Elixir 重新实现 Counter 计数器,其实能得到非常相似代码:

defmodule Counter do
  defstruct [:value]

  def new() do
    %Counter{value: 0}
  end

  def incr(counter) do
    %Counter{value: counter.value + 1}
  end
end

两者的代码在结构和风格上都非常相似,这主要还是因为 Elixir 的创造者 José Valim 之前是 Rails 社区的核心开发者,语言借鉴了很多 Ruby 中的设计,它的目的也是结合 Erlang 的强大功能和 Ruby 优秀的设计以及丰富的语法糖。

长连接

相信很多使用 Elixir 的人应该都读过 The Road to 2 Million Websocket Connections in Phoenix 这篇文章,在仅仅一台 40CPU、128Gb 的机器上同时处理 200 万个 WebSocket 长连接,虽然在生产环境中由于各种各样地因素,这量级并不现实,不过这是作者第一次感觉到 Elixir 和 Phoenix 在处理长连接方面的强大能力。

2-million-websocket

这种能力是作者开始在一些项目中尝试使用 Phoenix 的主要原因,最开始也一度认为有了 Ruby 优秀的语法和强大的性能就能使用 Elixir 和 Phoenix 解决各种问题。

随后由于我们主要做的是交易所业务,需要通过 WebSocket 向用户实时推送交易数据,所以选择使用 Elixir 来处理未来可能的大量长连接也顺理成章,只是到最后会发现这一点其实没那么重要。

放弃

当作者开始在项目中深入使用 Elixir 这门编程语言时,发现了很多难以调和的问题,在具体介绍这些问题之前,我们需要先让各位读者了解一下背景。

首先,项目最开始就选择使用微服务的方式进行设计和开发,最开始的三个核心系统都是选择使用 Elixir,服务之前的通信也都依赖于 RPC 调用,在最后,集群中的各种服务越来越多,到最后达到了 20~30 个,项目中的后端研发也从最开始的几个人到最后变成 20 多个。

社区

在 Elixir 和主要的 Web 框架 Phoenix 的使用过程中发现了非常多的问题,介绍具体问题之前,需要先给出我们的使用场景,也就是服务端集群的一些特点:

  1. 使用 GraphQL 对外暴露接口;
  2. 多种语言 Ruby、Elixir 和 Go 进行开发;
  3. 通过 Kubernetes 对容器进行编排;
  4. 服务间调用;
    1. 早期使用 RabbitMQ 和 Protobuf;
    2. 后期使用 gRPC 和 Protobuf;
  5. 服务发现和路由;
    1. 早期使用 RabbitMQ 和etcd;
    2. 后面使用 Linkerd/Istio;

在使用的过程中,其实我们遇到了以下的几个具体问题:

  1. 社区对主流的开源框架的支持并不完善;
  2. 主流框架的功能不够完备,迭代速度也比较慢;
  3. 有一些社区内特定的规则和实现比较难跨语言通用;

开源框架支持

gRPC-banne

由于项目最开始使用微服务架构,所以我们要对服务间的通信方式进行技术选型,因为系统中语言较多,所以需要跨语言调用,最后决定使用 RPC 时发现 Elixir 社区并没有主流 RPC 框架 gRPC、Thrift 的官方实现,Thrift 还有 Pinterest 提供的包,但是使用 Elixir 的 gRPC 库至今(2019-02-13)还有在生产环境使用请当心的提示,所以使用一些比较常用的服务时可能找不到官方提供的、甚至能用的客户端。

为了解决服务发现的问题,选择在 RabbitMQ 上实现一个简单的 RPC 框架,但是自研核心的中间件在使用过程中也遇到了很多问题,最主要的问题还是服务端宕机导致消息大量积压,重启后消费但是接收方已经不再处理这些超时已久的消息,从中也得到了教训 — 自研中间件的稳定性不经过长期的测试也打不到生产环境的要求。

主流框架

在 gRPC 的调研中却发现社区中最热门的 Web 框架 Phoenix 最新版本使用的 Cowboy 并不支持 HTTP/2.0,而覆写了 Cowboy 以及相关依赖的版本之后又发现 Websocket 没有办法正常使用,还好发现 Phoenix master 分支对 HTTP/2.0 进行了支持,最后在生产环境使用开发版本的 Phoenix 解决了这个问题,不过这也是作者头一回在生产环境上线开发版本的框架。

特定规则

我们最开始的一些 Elixir 服务使用序列化函数将一些结构体序列化成二进制后存储到数据库中,但是当我们想要从 Elixir 到其他语言时,我们却发现这些二进制的格式很难使用其他语言解析,最后在迁移的过程中只能保留一个专门用于处理类似请求的 Elixir 服务。

Phoenix 的 WebSocket 模块其实也引入了新的 Channel 概念,在 WebSocket 协议之上进行了一些简单的改造,如果一直在 Elixir 这个生态内倒是还好,一旦找到一些不得不迁移的原因就需要做大量额外的工作来兼容生态中的一些特定规则。

小结

在 2019 年二月份的 TIOBE 指数上,Elixir 已经跌出了前 50 名,具体的分数应该低于千分之一,在 GitHub Elixir Trending 上,每日 Star 最高的一般也都是 Elixir 语言自己。

elixir-github-trending

2019-02-13 的日排行上,只有 Elixir 语言本身获得了一些 Star,所以基本上只要有 Star 的 Elixir 仓库就能登上 Elixir 的趋势排行榜,由此可以看到社区的活跃度和语言的小众。

学习成本

作为函数式编程语言,Elixir 提供了非常优秀的并发支持,但是这些与 OOP 完全不同的特性 — 不可变、模式匹配、结构体与函数、进程与 GenServer 这些概念对于刚刚接触 Elixir 的人是非常难以理解和灵活使用的。

需要庆幸的是 Elixir 不是类似 Haskell 的纯函数式编程语言,所以我们能在 Elixir 中找到熟悉的命令式语法,即使是这样,由于大多数的工程师根本没有函数式编程语言的使用经验,上手并熟悉 Elixir 需要非常多的时间,哪怕是有经验的工程师想要适应 Elixir 的上下文和语境也比较困难。

从 OOP 到 FP

从面向对象到函数式思想的转换其实也比较困难,首先就是数据结构的不可变,在面向对象中,每一个对象都有一些实例变量,我们可以调用这些对象的方法来改变这些对象本身或者其他对象,这些方法最终都是属于某一个类的。

在 Elixir 中并不存在对象这一概念,与其类似的应该是结构体 struct,结构体和其他 Elixir 中的基本对象都是不可变的,一个结构体一旦被初始化就不能修改其中的参数,只能调用函数创建新的结构体并赋值回原有的变量:

variable = "draven"
variable = Module.func(variable, argument)

同时,Elixir 也没有类和方法的概念,所有的函数都是属于某一个模块的,如果想要实现面向对象中类似的方法调用,只能通过以下的方式实现:

defmodule MyInt do
  defstruct [:value]
  def add(%MyInt{value: v}, arg), do: %MyInt{value: v + arg}
end

将结构体作为函数的第一个参数传入,最后返回新的结构体,不过即使是这样,我们也没有办法改变原有内存空间中的数据。

控制流和模式匹配

Elixir 中引入的模式匹配也改变了很多语言经典的 if/elseswitch/case 控制流,刚刚使用 Elixir 的人可能非常自然地借鉴其他编程语言的经验写出如下的代码:

def complete(tx = %Transaction) do
  case tx.state do
     :success -> commit(tx)
     :failed -> rollback(tx)
     _ -> raise "Unknown State"
  end
end

但是使用模式匹配能够让上面的代码变得更加清晰易读,而且更容易增加新的条件:

def complete(%Transaction{state: :success} = tx), do: commit(tx)
def complete(%Transaction{state: :failed} = tx),  do: rollback(tx)
def complete(%Transaction{}), do: raise "Unknown State"

从使用常见控制流到使用模式匹配其实是一个思维转变的过程,当熟悉了 Elixir 这一套模式匹配的规则之后会发现这个功能非常好用,很多现代编程语言也都有比较强大的模式匹配系统,比如 Rust,但是 Elixir 无疑是做的比较优秀的一个。

面向对象和面向进程

最后一点其实是使用 Elixir 时最容易犯的的错误,也是最难改变的一个问题,在面向对象的框架下,我们长期都是在单线程或者多线程里调用依次调用多个对象的方法来实现业务逻辑,所有的业务都是围绕对象进行设计的。

但是 Elixir 却完全不同,作为面向并发的编程语言,它的应用在启动之后其实是一棵进程进程树,不同用户态进程之间会通过消息发送的方式进行通信,如果我们还是用面向对象的思路写 Elixir 的代码其实也可以工作,但是完全没有发挥语言和平台带来的加成。

作者觉得对于一个具有 OOP 经验的工程师来说,想要独自摸索并写出有 Elixir 味道的代码起码要有几个月的时间,一方面是因为写出 OOP 风格的 Elixir 代码也不会影响程序的正常运行,所以很难主动改变,另一方面确实因为思维的转变是非常困难的。

虽然改变思路学习新的编程范式非常困难,但是这种面向进程的设计能够让我们非常容易地使用 GenServer 引入一些缓存、计算和后台服务,而在面向对象的设计中引入类似的服务往往需要启动一个额外的进程。

小结

对于 Ruby 开发者来说,Elixir 的入门和上手会非常快,无论是 Elixir 还是 Phoenix 都大量借鉴了 Ruby 和 Rails 的设计,所以无论是在项目结构还是工具链方面都能找到很多类似的地方,但是想要真正掌握并且精通这门语言还是需要非常多练习和思考,总的来说,这是一门非常容易入门的编程语言,但是要掌握它的精髓是非常困难,不过学习 Elixir 过程中掌握的这些并发相关的知识和经验对于工程师来说也是一笔不小的财富。

市场

就作者之前的招聘经验来看,Elixir 的开发者主要由两部分组成,一部分是 Ruby 社区的开发者,发现 Elixir 有着与 Ruby 差不多的友好的语法并且有着比较优秀的性能所以开始使用 Elixir 作为主要或者辅助的编程语言,另外一部分是 Erlang 社区的开发者,这部分开发者之前可能从事电信、游戏和 IM 相关领域的开发,主要以 Erlang 为主,后来发现了同样运行在 Erlang BEAM 虚拟机上的 Elixir,所以学习了这门编程语言。

elixir-developers

无论是从工程师还是从公司的角度,想要使用 Elixir 作为主要的编程语言其实是一件成本非常高的事情;一方面,使用 Elixir 的工程师其实比较难找到合适的工作,而公司作为招聘方如果不能从市场上找到足够多的人就会选择其他的编程语言。

求职

大多数的工程师其实都不会把 Elixir 作为首要的编程语言,但是 Elixir 友好的语法和优秀的性能会吸引一部分 Rails 和 Erlang 的开发者,不过想在工程中主要使用这个编程语言还是比较困难,作者在拉钩网上(2019-02-13)搜索 Elixir 简单找了一下相关的工作:

lagou-elixi

全国跟 Elixir 相关的工作其实只有 20 个职位,仔细看了一下还有很多错误匹配的职位,例如前端开发工程师、Ruby 工程师等,与 Elixir 不相关的工作大概有 12 个,所以其实只有 8 个正在招聘的职位。

Elixir 作为一个非常小众的语言,它的圈子特别小,大多数人找工作也基本上都是靠朋友介绍或者社区内看到的一些招聘广告,所以这里的数据大家也可以仅做参考,但是无论如何将 Elixir 作为主要的编程语言并投入大量的精力去学习,从投资的角度来看,回报率是比较低的。

招聘

从招聘方的角度来说,虽然大多数的候选人都没有 Elixir 的背景,很多都是游戏公司的工程师,有很多年的 Erlang 开发经验,但是 Erlang 的经验对于 Elixir 来说没那么重要,这其中有一部分人也不想以后使用 Elixir 开发,还有一部分候选人都有 Rails 的开发经验,总的来说平均能力还是不错的。

最后给 offer 的工程师其实很多都没有 Elixir 的工程经验,但是基础相对来说都比较扎实,一般也都是入职之后再开始学习 Elixir,招聘难度确实比较大,培训和学习成本也比较高,当我们决定全面迁移到 Go 语言之后,招聘方面的选择就大了很多。

总结

Elixir 是一门内容非常丰富并且有趣的函数式编程语言,它的很多设计和特性都是其他语言中没有的,对于一个长期使用面向对象的开发者来说,这门编程语言确实能开拓眼界,但是作者认为它并不是一个能够在大中型项目中能够主要使用的编程语言,社区对于主流开源框架的支持不是特别充分,Ruby 相比之下虽然是小众语言,但是与 Elixir 相比而言社区却非常成熟,使用 Elixir 之后,很多时候都需要自己造轮子满足日常开发的需求,招聘难度相比其他主流语言也大很多。

如果想要在团队的人数在几个月内会超过 10 个人,那么选择使用这门语言一定要考虑是否能接受这门语言带来的问题;虽然比较适合小团队使用,但是也需要先看一下社区对依赖的中间件是否有比较好的支持,这些核心中间件没有经过长期的测试在生产环境中使用的风险非常大。

Reference

关于图片和转载

知识共享许可协议
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。

微信公众号

wechat-account-qrcode

关于评论和留言

如果对本文 Elixir 从入门到放弃 的内容有疑问,请在下面的评论系统中留言,谢谢。

原文链接:Elixir 从入门到放弃 · 面向信仰编程

Follow: Draveness · GitHub

Draveness

Go / Rails / Rust

Beijing, China draveness.me