概述

Elixir 是一种基于 Erlang 虚拟机的函数式,面向并行的通用语言, 它是一门通用语言,所以不仅可以用在擅长的高可用,高并发场景下,也可以用在 web 开发等场景下。 Erlang 诞生于 1986 年,爱立信。

有了 Erlang,为什么还要 Elixir? Erlang 毕竟诞生的早,虽然有很多优秀的特性,但是语法非常晦涩难懂,甚至没有支持 String Elixir 只是 Erlang 很简单的封装,不仅保留了 Erlang 所有的优秀特性,还提供了类似 Ruby 那样高效的语法。

和 Erlang 相比,Elixir 的语法有 2 个明显的优势

  • macro: 可以简化很多的代码
  • pipeline: |> 可以省却很多临时变量的定义

环境搭建

官方安装文档:https://elixir-lang.org/install.html 建议在 Unix-like 的系统下通过编译源码的方式安装,能够及时体验最新的特性

安装 Elixir 之前要先安装 Erlang

语言基础

在 Elixir 中,任何东西都可以理解为 *表达式+返回值*。 Elixir 中,任何数据不可改变,变量的赋值本质是把变量指向了其他的内存地址,不改变变量原来内存地址中的内容, 所以,等于(=)在 Elixir 中其实是 绑定(binding) 的意思,把右边的值绑定到左边的变量上,并返回右边的值

类型

Elixir 中的类型主要有以下几类:

Number

Elixir 中整数和小数统称为数值类型,整数的除法和求余用 div 和 rem 宏来实现

Atom

原子存在原子表中,不会被 GC 自动回收 true/false 在 Elixir 中是原子 :true/:false nil 在 Elixir 中是原子 :nil

Tuple

元素个数是固定的,适用于小的集合 通过 put_elem 修改 tuple 之后,其实返回的是一个新的 tuple,原来的 tuple 并没有被修改

List

list 可以表示为 [head|tail] 在 list 的末尾追加元素会导致 copy 整个 list,所以一般都在头部添加元素

Map

如果 key 是原子 ex. %{a: "xx", b: "yy"}; 否则 %{1 => "xx", "a string key" => "yy"}

Binary and Bitstring

长度是 8 的倍数的 Bitstring 也是 Binary

String

Elixir 中的 string 本质就是 Binary

Function

function 在 Elixir 中是一等公民,所以它也可以存放在变量中

Reference

BEAM 实例的引用

pid

Erlang process 的唯一标识

Keywork List

一种 list,每个元素都是一个包含 2 个元素的 tuple

IO List

一种深度自包含的结构,可以用来高效的在处理 IO 和网络数据 ex. 下面这个例子,文件写入时会分成 3 次,因为 a,b,c 的内存地址是不连续的 如果把 a,b,c 拼成一个字符串再写入文件的话,新的字符串会再次分配一次 a,b,c 所占用的内存量

{:ok, file} = :file.open("/tmp/tmp.txt", [:write, :raw])
a = "aaa"
b = "bbb"
c = "ccc"
output = [a, b, c]
:file.write(output)

用 io_list 可以避免上面的问题

{:ok, file} = :file.open("/tmp/tmp.txt", [:write, :raw])
a = "aaa"
b = "bbb"
c = "ccc"
output = [a, [b, [c]]]
:file.write(output)

IO.puts 输出时会拍平 io_list

控制流

一般语言中的流程控制就是判断(if, case…),循环(for, while…) Elixir 中虽然也有 if,case(通过 macro 来实现),但是尽量不要使用

Elixir 中通过模式匹配来实现判断,通过递归来实现循环

模式匹配的例子

通过不同的函数,实现变量的类型判断

defmodule ElixirIntro do
  def judge(x) when is_integer(x) do
    IO.puts("#{x} is integer")
  end

  def judge(x) when is_bitstring(x) do
    IO.puts("#{x} is string")
  end

  def judge(x) do
    IO.puts("#{x} is not integer and string")
  end
end

递归的示例

递归是 Elixir 中常用的技巧,通过递归可以写出简单易懂的代码 下面示例是累加求和的递归写法

defmodule ElixirIntro do
  def sum(n) when n <= 1 do
    n
  end

  def sum(n) do
    n + sum(n - 1)
  end
end

上面的递归写法虽然能完成功能,但是运行过程中消耗的内存很比较大,这种写法就是递归被人诟病的地方。

在 Elixir 中,我们应该使用尾递归的方式来完成循环,因为尾递归会被优化,只占用固定数量的内存,上面的示例如下:

defmodule ElixirIntro do
  def sum(total, n) when n <= 1 do
    total + n
  end

  def sum(total, n) do
    sum(total + n, n - 1)
  end
end

所谓尾递归,就是函数在最后只调用了自己

代码组织(模块和函数)

Elixir 的代码用 mix 来管理,通过 mix 创建,管理,发布工程。 代码主要是 module 和 function

$ mix new hello
$ cd hello
$ iex -S mix

Elixir 工程的代码都在 lib 文件夹下。

错误处理

错误处理是 Elixir 中的一级概念。

一般的错误处理思路都是尽可能的捕获错误(可预期和不可预期的):

  • 可预期的错误直接处理
  • 不可预期的错误捕获不到可能系统崩溃,捕获后一般也是直接跳过

Elixir 的核心能力之一是应对高并发,所以它的错误处理思路也不一样。 Elixir 错误处理的目的 不是降低错误的数量 ,而是 降低错误带来的影响 并且能够从错误中 自动恢复 。 所以,Elixir 的处理方式:

  • 可预期的错误,和传统方式一样,尽可能处理
  • 不可预期的错误,直接崩溃重启。会导致崩溃的地方尽快崩溃

Elixir 的观点:

  • 有些错误发生后,要想解决很困难,在发生错误后接着处理可能会导致更严重的错误
  • 很多运行时的错误,极难重现,大部分都能通过重启来解决

总结

Elixir 虽然在它的领域有先天的优势,但是肯定也有不足之处:

  • speed: 性能不是 Erlang 平台的优势,毕竟有一层 BEAM 虚拟机, 高并发 != 高性能
  • ecosystem: Erlang/Elixir 的生态远没有其他语言成熟(比如 java, javascript, ruby 等)

Elixir 不是万能的,但是学习 Elixir 可以打开你的视野,特别是对于一直使用面向对象语言的同学, 学习 Elixir 可以让你以另外一种视角看待程序设计。