Skip to content

Elixir 速查表

基于 Erlang VM 构建的函数式并发语言。

01

入门

基础与 IEx

Elixir 运行在 Erlang VM (BEAM) 上,以并发和容错性著称。原子是常量,其名称即为其值。<> 连接字符串。元组大小固定,列表是链表。

elixir
# IEx interactive shell
# run: iex

# simple values
iex> 1 + 2
3
iex> "hello" <> " world"
"hello world"

# atoms (constants where name is value)
iex> :ok
:ok
iex> :error
:error

# tuples
iex> {:ok, 42}
{:ok, 42}

# lists
iex> [1, 2, 3]
[1, 2, 3]

Hello World

IO.puts 输出并换行。#{expr} 将表达式插值到字符串中。.exs 文件是脚本(直接运行);.ex 文件需编译。IO.inspect 是调试利器。

elixir
# hello.exs
IO.puts("Hello, World!")

# run: elixir hello.exs
# or in iex: iex hello.exs

# string interpolation with #{}:
name = "Elixir"
IO.puts("Hello, #{name}!")
# Hello, Elixir!

# IO.inspect shows any value (useful for debugging):
IO.inspect([1, 2, 3], label: "nums")

注释与脚本

Elixir 仅支持以 # 开头的单行注释。.exs 用于脚本,.ex 用于可编译模块。在 IEx 中,r Module 重载单个模块,recompile() 重新构建 Mix 项目。

elixir
# single line comment only
# Elixir has no multi-line comments

# script file (.exs) - executed directly
# elixir script.exs

# compiled module file (.ex) - compiled then run
# elixirc file.ex

# in IEx, reload a single module:
iex> r MyModule

# recompile the whole project:
iex> recompile()

帮助与文档

h 显示文档,i 内省值的类型,s 显示函数规范,b 列出 behaviour 回调,t 打印类型定义。这些是交互式学习的重要工具。

elixir
# in IEx:
iex> h Enum.map        # docs for Enum.map/2
iex> h Enum            # module docs
iex> i "hello"         # type info about a value
iex> b GenServer       # list behaviour callbacks
iex> s Enum.map        # function spec
iex> t Enum.t          # print type definitions

# exit IEx:
iex> System.halt(0)
# or press Ctrl+C twice

项目结构

Mix 项目使用 lib/ 存放源码,test/ 存放 ExUnit 测试,mix.exs 存放配置。模块名映射到文件路径(MyApp.Greeter 位于 lib/my_app/greeter.ex),便于查找。

elixir
# Mix project layout:
my_app/
  lib/           # source code (.ex)
    my_app.ex
    my_app/greeter.ex
  test/          # ExUnit tests (.exs)
    my_app_test.exs
  mix.exs        # project config & dependencies
  config/        # config files
    config.exs

# module names map to file paths:
# MyApp.Greeter -> lib/my_app/greeter.ex
defmodule MyApp.Greeter do
  def hello(name), do: "Hi, #{name}"
end
02

基本类型

原子

原子是不可变常量,其名称即为其值。true、false 和 nil 都是原子。不要从不可信输入创建原子——原子表不会被垃圾回收,可能造成内存泄漏。

elixir
# atoms are constants where the name IS the value
:ok
:error
:success
:true   # boolean true is an atom
:false  # boolean false is an atom
nil     # nil is also an atom

# create from a string:
String.to_atom("hello")   # :hello

# atoms compared by name (alphabetical):
:apple < :banana   # true

# existing? check:
Atom.to_string(:ok)   # "ok"

数字

整数是任意精度的(不会溢出)。/ 始终返回浮点数;用 div/2 和 rem/2 进行整数运算。支持十六进制 (0x)、八进制 (0o) 和二进制 (0b) 字面量。

elixir
# integers (arbitrary precision)
42
-7
0x1F    # hex = 31
0o17    # octal = 15
0b1010  # binary = 10

# floats
3.14
-0.5
1.0e3   # 1000.0

# division always returns a float:
7 / 2       # 3.5
div(7, 2)   # 3  (integer division)
rem(7, 3)   # 1  (remainder)

# rounding:
round(3.6)  # 4
trunc(3.9)  # 3

字符串与二进制

双引号字符串是二进制(UTF-8 字节);单引号是字符列表(很少使用)。Heredoc 使用三个双引号。不要混用——String 函数期望二进制。

elixir
# double-quoted strings are UTF-8 binaries
"hello"
"Elixir"           # UTF-8 supported
"#{1 + 1} cats"    # "2 cats"

# single-quoted are charlists (list of codepoints)
'hello' == [104, 101, 108, 108, 111]   # true

# heredoc for multiline strings:
"""
line 1
line 2
"""

is_binary("hi")   # true
is_list('hi')     # true

布尔值与 nil

只有 false 和 nil 是 falsy——其他所有值(包括 0、[]、"")都是 truthy。and/or/not 要求布尔值;&&/||/! 接受任意值并返回操作数本身。

elixir
true
false
nil

# only false and nil are falsy:
if nil, do: "yes", else: "no"   # "no"
if 0, do: "yes", else: "no"     # "yes" (0 is truthy!)
if [], do: "yes", else: "no"    # "yes" (empty list is truthy)

# strict boolean ops (require booleans):
true and false   # false
true or false    # true
not true         # false

# truthy ops (any value, return the value):
nil && 1     # nil
1 && 2       # 2
nil || "x"   # "x"

列表与元组概览

列表是链表(头部插入快,随机访问慢)。元组在连续内存中存储固定大小的值(按下标访问快)。关键字列表是 {atom, value} 元组的列表;映射允许任意键。

elixir
# list (linked list)
[1, 2, 3]
[head | tail] = [1, 2, 3]   # head=1, tail=[2,3]
[:a, "b", 3]                # heterogeneous ok

# tuple (fixed-size, contiguous memory)
{:ok, 42}
{1, 2, 3}
tuple_size({:a, :b, :c})    # 3

# keyword list (list of 2-tuples with atom keys)
[name: "Alice", age: 30]

# map (any keys)
%{"name" => "Bob", :age => 25}
03

运算符

算术运算符

/ 始终返回浮点数;用 div/2 进行整数除法。rem/2 取被除数的符号,Integer.mod/2 始终非负。Integer.pow/2(1.12+)做整数幂运算。

elixir
1 + 2      # 3
5 - 3      # 2
2 * 4      # 8
10 / 2     # 5.0  (always float)
div(7, 2)  # 3    (integer division)
rem(7, 3)  # 1    (remainder, sign of dividend)
abs(-5)    # 5
round(3.6) # 4
trunc(3.9) # 3

# power (Elixir 1.12+):
Integer.pow(2, 10)  # 1024
:math.pow(2, 10)    # 1024.0 (float)

比较运算符

== 比较值,=== 同时比较值和类型。Elixir 在所有类型间有全序关系(number < atom < ... < list < bitstring),因此任意两个值都能比较且不会报错。

elixir
1 == 1.0     # true   (value equality, ignores type)
1 === 1.0    # false  (strict, also checks type)
1 != 2       # true
1 !== 1.0    # true
2 > 1        # true
2 >= 2       # true
1 < 2        # true
1 <= 2       # true

# total ordering across types:
# number < atom < ref < fun < port < pid < tuple < map < list < bitstring
:atom > 1    # true
[1] > %{}    # true

逻辑运算符

and/or/not 要求布尔参数(否则报错)并短路求值。&&/||/! 接受任意值,只把 false 和 nil 视为 falsy,并返回操作数本身而非布尔值。

elixir
# strict (require boolean operands):
true and false   # false
true or false    # true
not true         # false

# truthy (any operand, return the operand):
nil && 1          # nil
1 && 2            # 2
nil || "default"  # "default"
false || 5        # 5
!nil              # true
!1                # false

# short-circuit evaluation:
false and raise("never")   # false (right side skipped)
true or raise("never")     # true  (right side skipped)

字符串连接

<> 连接二进制(字符串);++ 连接列表(包括字符列表)。-- 移除右侧每个元素的第一个匹配。可读性上优先用插值而非反复 <>。

elixir
"hello" <> " " <> "world"   # "hello world"

# interpolation (preferred):
name = "Alice"
"Hi, #{name}!"               # "Hi, Alice!"
"Sum: #{1 + 2}"              # "Sum: 3"

# charlist concatenation:
'abc' ++ 'de'   # 'abcde'

# list concatenation and subtraction:
[1, 2] ++ [3, 4]   # [1, 2, 3, 4]
[1, 2, 3] -- [2]   # [1, 3]  (removes first match)

匹配运算符

= 将右侧与左侧模式匹配,绑定变量。若不匹配则抛出 MatchError。变量可重新绑定(指向新值),但底层数据是不可变的。

elixir
# = is pattern matching, not assignment
x = 1
1 = x          # ok (matches, since x is 1)
2 = x          # raises MatchError

# destructuring:
{a, b} = {1, 2}
a   # 1
b   # 2

[left | rest] = [1, 2, 3]   # left=1, rest=[2,3]

# variables can be rebound (data itself is immutable):
x = 1
x = 2          # rebinds x to 2

成员判断与其他

in/2 检查在列表、范围、映射(键)和二进制(子串)中的成员关系。| 向列表头部添加元素,也用于模式匹配拆分 head/tail。|> 是管道运算符(见专章)。

elixir
# in/2 checks membership in an enumerable:
2 in [1, 2, 3]        # true
:b in [:a, :b, :c]    # true
"x" in "text"         # true (substring in binary)

# range membership:
5 in 1..10            # true

# | prepends to a list:
[0 | [1, 2, 3]]       # [0, 1, 2, 3]

# also used to split head/tail:
[head | tail] = [1, 2, 3]

# |> is the pipe operator (see Pipe section):
"hi" |> String.upcase()   # "HI"
04

模式匹配

匹配运算符

= 将模式中的变量绑定到右侧的值。在同一模式中重复使用某变量会强制这些值相等。_ 匹配(并丢弃)任意值。

elixir
# = matches right against left
x = 42
{a, b, c} = {1, 2, 3}
[a, b, c] = [:x, :y, :z]

# reusing a variable in one pattern forces equality:
{n, n} = {1, 1}    # ok, n = 1
{n, n} = {1, 2}    # MatchError (1 != 2)

# ignore values with _:
{_, b, _} = {1, 2, 3}   # b = 2

# bind multiple at once:
{_, {x, y}} = {:ok, {3, 4}}   # x=3, y=4

钉住运算符 (^)

^var 钉住变量,使其按当前值匹配而不是重新绑定。在 case 子句和函数头中按已有值匹配时不可或缺。

elixir
x = 1

# ^ pins a variable: match its current value, don't rebind
^x = 1          # ok (x already 1)
^x = 2          # MatchError

case {1, 2} do
  {^x, second} -> "matched, second=#{second}"
  _ -> "no match"
end
# "matched, second=2"

# in function heads:
def same_as_x?(^x), do: true
def same_as_x?(_), do: false

匹配元组

元组按大小和内容匹配。{:ok, value}、{:error, reason} 等标签元组是返回值的惯用法。case 可用独立子句分别处理每种形状。

elixir
{:ok, value} = {:ok, 42}
value   # 42

{:error, reason} = {:error, :not_found}
reason  # :not_found

# size must match:
{a, b} = {1, 2, 3}   # MatchError (size 2 vs 3)

# nested matching:
{:user, {name, age}} = {:user, {"Alice", 30}}
name   # "Alice"

# tagged tuples for results:
case File.read("missing.txt") do
  {:ok, content}      -> content
  {:error, :enoent}   -> "file not found"
end

匹配列表(head/tail)

[head | tail] 将列表分解为首元素和剩余部分。列表按结构匹配——递归遍历就用它。空列表 [] 只匹配空列表。

elixir
# head and tail:
[head | tail] = [1, 2, 3]
head   # 1
tail   # [2, 3]

# fixed first elements + rest:
[a, b | rest] = [1, 2, 3, 4]
a      # 1
b      # 2
rest   # [3, 4]

# empty list matches only []:
[] = []    # ok
[] = [1]   # MatchError

# classic recursion pattern:
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)

匹配映射

映射模式只检查你点名的键——即使存在额外键,子集匹配也会成功。用 ^key => value 匹配动态键。结构体按名称和字段匹配。

elixir
# match on specific keys (subset is fine):
%{name: name} = %{name: "Alice", age: 30}
name   # "Alice"

# match a specific value:
%{status: :active} = %{status: :active, id: 1}   # ok
%{status: :closed} = %{status: :active}          # MatchError

# variable as a key requires pinning:
key = :name
%{^key => value} = %{name: "Bob"}
value   # "Bob"

# struct matching (also a map):
%User{name: n} = %User{name: "Carol", age: 40}

函数中的模式匹配

函数子句按顺序尝试,第一个匹配的头获胜。模式匹配 + 卫语句可替代 if/else 做分派。务必包含一个兜底子句(否则无匹配时报 FunctionClauseError)。

elixir
defmodule Geometry do
  # clauses tried top to bottom; first match wins:
  def area({:rectangle, w, h}), do: w * h
  def area({:circle, r}), do: 3.14 * r * r
  def area({:square, s}), do: s * s
  def area(_), do: {:error, :unknown_shape}
end

Geometry.area({:rectangle, 3, 4})   # 12
Geometry.area({:circle, 2})         # 12.56
Geometry.area({:triangle, 3, 4})    # {:error, :unknown_shape}

# guards add conditions:
def classify(n) when n < 0, do: :negative
def classify(0), do: :zero
def classify(n) when n > 0, do: :positive
05

不可变性

不可变数据

所有 Elixir 数据结构都是不可变的——函数返回修改后的副本,从不修改原值。变量是值的绑定;重新绑定只是把变量指向别处,旧值不受影响。

elixir
# data is never mutated; operations return new data
list = [1, 2, 3]
List.replace_at(list, 0, 99)   # [99, 2, 3]
list                           # [1, 2, 3] (unchanged)

map = %{a: 1}
Map.put(map, :b, 2)            # %{a: 1, b: 2}
map                            # %{a: 1} (unchanged)

# strings too:
s = "hello"
String.upcase(s)   # "HELLO"
s                  # "hello"

列表操作返回新列表

用 | 头部插入是 O(1),因为列表是单向链表且共享尾部。用 ++ 追加是 O(n),需遍历左列表。原列表被共享而非复制——高效且安全。

elixir
original = [1, 2, 3]

# prepend (fast, O(1)):
[0 | original]         # [0, 1, 2, 3]
original               # [1, 2, 3]

# append (slow, O(n)):
original ++ [4]        # [1, 2, 3, 4]
original               # [1, 2, 3]

# concatenation:
[1, 2] ++ [3, 4]       # [1, 2, 3, 4]

映射更新

%{map | key: val} 更新已有键(不存在则报错)。Map.put_new/3 仅在键不存在时添加。这些都返回新映射;原映射不变。映射内部共享结构以提升效率。

elixir
m = %{name: "Alice", age: 30}

# Map.put (add or update):
Map.put(m, :age, 31)        # %{name: "Alice", age: 31}

# update syntax (key MUST exist):
%{m | age: 31}              # %{name: "Alice", age: 31}
# %{m | height: 170}        # KeyError (height not present)

# add only if missing:
Map.put_new(m, :role, "admin")   # adds :role
Map.put_new(m, :age, 99)         # unchanged (age exists)

# delete:
Map.delete(m, :age)         # %{name: "Alice"}

变量不可变

重新绑定变量使其指向新值,但绝不修改旧值。其他指向旧值的绑定(如 y)仍然看到旧值。任何数据结构都没有原地修改的 API。

elixir
# variables bind to values; rebinding is allowed but
# the bound VALUE never changes
x = [1, 2, 3]
y = x              # y points to the same list
x = [4, 5]         # x rebound, y still [1, 2, 3]
y                  # [1, 2, 3]

# there is NO in-place mutation API:
# list[0] = 99    -- not valid Elixir

# to "update", rebind the variable:
x = [99 | tl(x)]   # x now points to a new list

共享与效率

持久化数据结构共享未修改部分,因此“复制”很廉价(列表头部插入 O(1),映射更新约 O(log n))。不可变性让跨进程共享无需锁或复制即可安全进行。

elixir
# immutability enables safe structural sharing:
base = [1, 2, 3, 4, 5]
shared = [0 | base]    # [0, 1, 2, 3, 4, 5]
# `shared` reuses `base`'s nodes—no copy of the tail

# maps also share internally; updates are ~O(log n):
m = %{a: 1, b: 2, c: 3}
m2 = Map.put(m, :d, 4)   # shares most of m

# because data is immutable, sharing is always safe—
# no defensive copies needed across processes.
pids = Enum.map(1..100, fn _ -> spawn(fn -> m end) end)
06

列表

头部与尾部

列表是单向链表:head 是首元素,tail 是剩余部分(一个列表)。hd/1 和 tl/1 提取它们(空列表报错)。头部插入 O(1);length/1 是 O(n) 因为要遍历。

elixir
[head | tail] = [1, 2, 3]
head          # 1
tail          # [2, 3]

hd([1, 2, 3]) # 1
tl([1, 2, 3]) # [2, 3]

# empty list has no head:
# hd([])   # raises ArgumentError

# prepend a first element:
[0 | [1, 2, 3]]   # [0, 1, 2, 3]

# length is O(n):
length([1, 2, 3])  # 3

连接与减法

++ 连接(对左列表 O(n))。-- 移除右侧每个元素的第一个匹配。List.flatten/1 展开嵌套。大多数操作优先用 Enum 而非 List 模块。

elixir
[1, 2] ++ [3, 4]    # [1, 2, 3, 4]
[] ++ [1]           # [1]

# subtraction removes first occurrence of each right element:
[1, 2, 2, 3, 2] -- [2]   # [1, 2, 3, 2]  (only first 2)
[1, 2, 3] -- [4]         # [1, 2, 3]      (no-op)

# flatten nested lists:
List.flatten([1, [2, [3, 4]], 5])   # [1, 2, 3, 4, 5]

# fold (left):
List.foldl([1, 2, 3], 0, fn x, acc -> x + acc end)  # 6

列表推导

for 是推导式:生成器(<-)、过滤条件和 do 表达式。默认返回列表;into: 改变目标容器(映射、字符串等)。在一个表达式中实现转换和过滤很强大。

elixir
# basic:
for n <- [1, 2, 3], do: n * 2        # [2, 4, 6]

# with filter:
for n <- 1..10, rem(n, 2) == 0, do: n   # [2, 4, 6, 8, 10]

# multiple generators:
for x <- [:a, :b], y <- [1, 2], do: {x, y}
# [a: 1, a: 2, b: 1, b: 2]

# into: change the result container:
for n <- [1, 2, 3], into: %{}, do: {n, n * n}
# %{1 => 1, 2 => 4, 3 => 9}

# build a string:
for c <- ?a..?c, into: "", do: <<c>>   # "abc"

List 模块函数

List 模块提供基于下标和结构的操作。List.first/last 是 O(n)。List.wrap/1 将输入归一化为列表(选项解析时很方便)。一般操作用 Enum。

elixir
List.first([1, 2, 3])         # 1
List.last([1, 2, 3])          # 3
List.delete([1, 2, 3], 2)     # [1, 3]
List.delete_at([1, 2, 3], 1)  # [1, 3]
List.insert_at([1, 2, 3], 1, 9)   # [1, 9, 2, 3]
List.replace_at([1, 2, 3], 1, 9)  # [1, 9, 3]

# wrap (ensure list):
List.wrap(nil)    # []
List.wrap(1)      # [1]
List.wrap([1, 2]) # [1, 2]

# duplicate:
List.duplicate(:x, 3)   # [:x, :x, :x]

枚举列表

Enum 适用于任何可枚举对象(列表、映射、范围)。map/filter/reduce 是主力。chunk_every/2 按大小分组;zip/2 配对两个列表的元素。

elixir
Enum.map([1, 2, 3], fn x -> x * 10 end)        # [10, 20, 30]
Enum.filter([1, 2, 3, 4], fn x -> x > 2 end)    # [3, 4]
Enum.reduce([1, 2, 3], 0, &+/2)                 # 6
Enum.find([1, 2, 3], fn x -> x > 1 end)         # 2

# membership:
1 in [1, 2, 3]                # true
Enum.member?([1, 2, 3], 2)    # true

# chunking:
Enum.chunk_every([1, 2, 3, 4], 2)   # [[1, 2], [3, 4]]

# zip:
Enum.zip([:a, :b], [1, 2])   # [{:a, 1}, {:b, 2}]

展平与折叠

List.flatten/1 移除嵌套。Enum.reduce/3 是惯用的左折叠。List.foldr/3 从右折叠。用头部插入折叠可在 O(n) 内高效构建反序列表。

elixir
# flatten:
List.flatten([1, [2, 3], [4, [5]]])   # [1, 2, 3, 4, 5]

# flatten with a tail:
List.flatten([1, [2]], [3, 4])        # [1, 2, 3, 4]

# fold (Enum.reduce is a left fold):
Enum.reduce([1, 2, 3], 0, fn x, acc -> acc + x end)   # 6

# right fold:
List.foldr([1, 2, 3], [], fn x, acc -> [x | acc] end) # [1, 2, 3]

# building a reversed list with reduce + prepend:
Enum.reduce([1, 2, 3], [], fn x, acc -> [x | acc] end) # [3, 2, 1]
07

元组

创建元组

元组在连续内存中存放固定数量的元素(按下标 O(1) 访问)。用于小型、固定形状的聚合——尤其是 {:ok, value} 等标签元组——而非可增长的集合。

elixir
{:ok, 42}
{1, 2, 3}
{}
{:point, 3, 4}

# from a list:
List.to_tuple([:a, :b, :c])   # {:a, :b, :c}

# duplicate elements:
Tuple.duplicate(:x, 3)        # {:x, :x, :x}

# size (O(1)):
tuple_size({:a, :b, :c})      # 3

访问元素

elem/2 按 0 起始下标读取,复杂度 O(1)。put_elem/3 返回新元组(不可变)。模式匹配比按下标访问更地道——更清晰且自解释。

elixir
t = {:ok, "hello", 42}

elem(t, 0)    # :ok
elem(t, 1)    # "hello"
elem(t, 2)    # 42

# tuples are 0-indexed; out of range raises:
# elem(t, 5)   # ArgumentError

# put_elem returns a NEW tuple:
put_elem(t, 1, "world")   # {:ok, "world", 42}
t                          # unchanged: {:ok, "hello", 42}

# pattern matching is preferred over index access:
{:ok, msg, _} = t
msg   # "hello"

标签元组(ok/error)

标签元组({:ok, value}、{:error, reason})是 Elixir 不用异常表达成功/失败的惯用法。case 和 with 都能解构它们。务必同时处理两个分支。

elixir
# idiomatic return values:
def divide(_a, 0), do: {:error, :divide_by_zero}
def divide(a, b), do: {:ok, div(a, b)}

# handle with case:
case divide(10, 2) do
  {:ok, result}     -> "result: #{result}"
  {:error, reason}  -> "error: #{reason}"
end
# "result: 5"

# with chains matches across steps:
with {:ok, a} <- maybe_a(),
     {:ok, b} <- maybe_b() do
  a + b
end

更新元组

元组不可变;每次“更新”都返回新元组。追加/插入/删除是 O(n),因为整个元组要复制。如果频繁增删元素,改用列表或映射。

elixir
t = {1, 2, 3}

# put_elem returns a new tuple:
put_elem(t, 0, 99)        # {99, 2, 3}

# tuples are immutable:
t                         # {1, 2, 3}

# append/prepend/insert create a NEW tuple (O(n)):
Tuple.append(t, 4)        # {1, 2, 3, 4}
Tuple.insert_at(t, 1, 99) # {1, 99, 2, 3}

# delete:
Tuple.delete_at(t, 1)     # {1, 3}

Tuple 模块

Tuple 模块提供结构性操作(append、insert_at、delete_at、duplicate)。Tuple.to_list/1 和 List.to_tuple/1 在两者间转换。tuple_size/1 是 O(1)。

elixir
t = {:a, :b, :c}

Tuple.append(t, :d)         # {:a, :b, :c, :d}
Tuple.delete_at(t, 0)       # {:b, :c}
Tuple.duplicate(:x, 3)      # {:x, :x, :x}
Tuple.insert_at(t, 1, :z)   # {:a, :z, :b, :c}

# conversions:
Tuple.to_list(t)            # [:a, :b, :c]
List.to_tuple([1, 2, 3])    # {1, 2, 3}

# size:
tuple_size(t)               # 3
08

关键字列表

创建关键字列表

关键字列表是 {atom, value} 元组的列表,有特殊的字面语法。键必须是原子,且允许重复。它本质就是列表,因此所有列表操作都适用(访问是 O(n))。

elixir
# literal syntax (list of 2-tuples with atom keys):
[name: "Alice", age: 30]
# equivalent to:
[{:name, "Alice"}, {:age, 30}]

# duplicate keys allowed:
[a: 1, a: 2]   # [a: 1, a: 2]

# empty:
[]

# a keyword list is just a list:
is_list([a: 1])      # true
length([a: 1, b: 2]) # 2

访问值

用 kw[:key] 访问返回第一个匹配值(不存在则为 nil)。Keyword.get_values/2 返回重复键的所有值。关键字列表每次查找 O(n)——频繁访问请用映射。

elixir
kw = [name: "Alice", age: 30, role: :admin]

# access first value for a key:
kw[:name]    # "Alice"
kw[:age]     # 30
kw[:missing] # nil

# Keyword.get with a default:
Keyword.get(kw, :missing, "default")  # "default"

# get ALL values for a key (when duplicates exist):
Keyword.get_values([a: 1, a: 2], :a)  # [1, 2]

# strict fetch (raises if missing):
Keyword.fetch!(kw, :name)   # "Alice"

Keyword 模块

Keyword 模块镜像了许多 Map 函数,但操作的是元组列表。put/3 是在前面新增键值对(不会替换已有——替换需先 delete 再 put)。操作都是 O(n)。

elixir
kw = [a: 1, b: 2, c: 3]

Keyword.keys(kw)          # [:a, :b, :c]
Keyword.values(kw)        # [1, 2, 3]
Keyword.has_key?(kw, :a)  # true

Keyword.put(kw, :d, 4)    # [a: 1, b: 2, c: 3, d: 4]
Keyword.delete(kw, :b)    # [a: 1, c: 3]

# merge (right side wins on conflicts):
Keyword.merge([a: 1], [a: 99, b: 2])  # [a: 99, b: 2]

# take/drop:
Keyword.take(kw, [:a, :c])   # [a: 1, c: 3]

关键字列表与映射

关键字列表用于有序、原子键的选项列表(如函数选项)。映射用于任意键类型和快速查找的通用键值存储。不要把关键字列表当作主数据结构。

elixir
# Keyword list: ordered, atom keys only, duplicates, O(n)
[name: "Alice", age: 30]

# Map: unordered, any keys, no duplicates, ~O(log n)
%{name: "Alice", age: 30}

# use keyword lists for function options (idiomatic):
String.split("a,b,c", ",", trim: true)

# use keyword lists for small ordered config:
Application.get_env(:my_app, :key, [])

# use maps for:
# - key/value data with mixed-type or string keys
# - frequent lookups

常见用例

关键字列表作为函数最后一个参数的选项非常合适(如 trim: true)。do/end 块语法本质就是关键字列表:if(true, do: x, else: y)。配置文件大量使用。

elixir
# function options (most common):
def greet(name, opts \\ []) do
  prefix = Keyword.get(opts, :prefix, "Hello")
  "#{prefix}, #{name}!"
end
greet("Alice", prefix: "Hi")   # "Hi, Alice!"

# config files:
config :my_app, MyRepo,
  pool_size: 10,
  timeout: 5000

# do/end block sugar is a keyword list:
if true, do: :yes, else: :no
# is shorthand for:
if(true, do: :yes, else: :no)
09

映射

创建映射

映射存放任意键类型的键值对。%{key: val} 语法要求原子键;其他类型用 key => val。map_size/1 是 O(1)。映射是主要的键值数据结构。

elixir
# atom keys (shorthand):
%{name: "Alice", age: 30}

# any keys (=>):
%{"name" => "Bob", 1 => :one, :ok => true}

# mixed keys:
%{:atom => 1, "string" => 2}

# from a keyword list:
Enum.into([a: 1, b: 2], %{})   # %{a: 1, b: 2}

# empty:
%{}

# size (O(1)):
map_size(%{a: 1, b: 2})   # 2

访问值

m[:key] 在键缺失时返回 nil(安全)。m.key(点号)仅限原子键,缺失则抛 KeyError——键是必需时用它。Map.fetch/2 返回 {:ok, val} 或 :error,便于显式处理。

elixir
m = %{name: "Alice", age: 30, role: :admin}

# bracket access (nil if missing):
m[:name]    # "Alice"
m[:missing] # nil

# dot access (atom keys only, raises if missing):
m.name      # "Alice"
# m.missing  # KeyError

# strict fetch:
Map.fetch!(m, :age)    # 30
Map.fetch(m, :missing) # :error

# with a default:
Map.get(m, :missing, "default")  # "default"

更新映射

%{map | key: val} 更新已有键(不存在则 KeyError)。Map.put/3 添加或更新。Map.merge/2 合并(冲突时右侧优先)。都返回新映射;原映射不变。

elixir
m = %{name: "Alice", age: 30}

# update existing key (raises if missing):
%{m | age: 31}              # %{name: "Alice", age: 31}

# add or update:
Map.put(m, :age, 31)        # %{name: "Alice", age: 31}
Map.put(m, :role, :admin)   # adds :role

# add only if missing:
Map.put_new(m, :role, :admin)

# delete:
Map.delete(m, :age)         # %{name: "Alice"}

# merge (right side wins on conflicts):
Map.merge(%{a: 1}, %{a: 99, b: 2})   # %{a: 99, b: 2}

Map 模块函数

Map.keys/values 返回列表(顺序不保证)。take/drop 选择或移除键。put_in/get_in/update_in 能优雅地导航和修改嵌套结构。

elixir
m = %{a: 1, b: 2, c: 3}

Map.keys(m)           # [:a, :b, :c] (order not guaranteed)
Map.values(m)         # [1, 2, 3]
Map.has_key?(m, :a)   # true
Map.take(m, [:a, :c]) # %{a: 1, c: 3}
Map.drop(m, [:b])     # %{a: 1, c: 3}

# transform:
Map.new([{:a, 1}, {:b: 2}])   # %{a: 1, b: 2}
Map.to_list(m)                # [a: 1, b: 2, c: 3]

# nested update:
users = %{alice: %{age: 30}}
put_in(users, [:alice, :age], 31)   # %{alice: %{age: 31}}

模式匹配映射

映射模式只匹配你点名的键——额外键无所谓。匹配变量键时用 ^ 钉住。这让映射非常适合解构 API 响应和配置。

elixir
# subset matching (extra keys ignored):
%{name: name} = %{name: "Alice", age: 30}
name   # "Alice"

# match a specific value:
%{status: :active} = %{status: :active, id: 1}   # ok

# variable key (must be pinned):
key = :name
%{^key => value} = %{name: "Bob"}
value   # "Bob"

# in case clauses:
case response do
  %{status_code: 200, body: body} -> body
  %{status_code: code}            -> {:error, code}
end

结构体

结构体是带固定键集合和默认值的标签映射,用 defstruct 定义。它们携带模块名(__struct__)并在编译期强制键——用于领域实体。

elixir
defmodule User do
  defstruct name: "anon", age: 0, role: :user
end

# create:
%User{name: "Alice", age: 30}
# %User{age: 30, name: "Alice", role: :user}

# access:
u = %User{name: "Bob"}
u.name          # "Bob"
u.__struct__    # User

# update (existing fields only):
%{u | age: 25}  # %User{age: 25, name: "Bob", role: :user}

# unknown key raises at compile time:
# %User{height: 180}   # KeyError / compile error

# pattern match:
%User{name: n} = u
n   # "Bob"
10

Enum

map 与 each

map/2 将每个元素转换为新列表(长度相同)。each/2 用于副作用,返回 :ok。Enum 适用于任何可枚举对象:列表、映射(以 {k,v} 形式)、范围等。

elixir
Enum.map([1, 2, 3], fn x -> x * 2 end)        # [2, 4, 6]
Enum.map([1, 2, 3], &(&1 * 2))                   # [2, 4, 6]  (capture)

# each returns :ok (side effects only):
Enum.each([1, 2, 3], fn x -> IO.puts(x) end)
# prints 1, 2, 3; returns :ok

# with index:
Enum.with_index([:a, :b, :c])   # [{:a, 0}, {:b, 1}, {:c, 2}]

# over a map (yields {key, value} pairs):
Enum.map(%{a: 1, b: 2}, fn {k, v} -> {k, v * 10} end)
# [a: 10, b: 20]

filter 与 reject

filter 保留函数返回 truthy 的元素;reject 是其反操作。要在一次遍历中既过滤又映射,可用 for 推导或 Enum.flat_map 配合条件列表。

elixir
Enum.filter([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end)   # [2, 4]
Enum.reject([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end)   # [1, 3, 5]

# truthy filter (drop nil/false):
Enum.filter([nil, 1, false, 2], & &1)   # [1, 2]

# filter + map in one pass via flat_map:
Enum.flat_map([1, 2, 3], fn x ->
  if x > 1, do: [x * 10], else: []
end)
# [20, 30]

reduce

reduce/3 将可枚举对象折叠为单个累加器。传入显式初始值以处理空列表。reduce/2 不带初值时使用第一个元素(空列表报错)。

elixir
# sum:
Enum.reduce([1, 2, 3, 4], 0, fn x, acc -> acc + x end)   # 10

# shorthand with a captured operator:
Enum.reduce([1, 2, 3, 4], 0, &+/2)   # 10

# build a map:
Enum.reduce([{:a, 1}, {:b, 2}], %{}, fn {k, v}, acc ->
  Map.put(acc, k, v * 2)
end)
# %{a: 2, b: 4}

# without an initial acc: uses the first element (raises on empty):
Enum.reduce([1, 2, 3], fn x, acc -> x + acc end)   # 6

find 与 find_value

find 返回第一个匹配的元素;find_value 返回函数的 truthy 结果(配合哨兵值做转换很方便)。any?/all? 一旦得到答案就短路,不必扫描整个列表。

elixir
Enum.find([1, 2, 3, 4], fn x -> x > 2 end)        # 3 (first match)
Enum.find([1, 2, 3], fn x -> x > 5 end)             # nil (no match)

# find_value returns the FUNCTION result, not the element:
Enum.find_value([1, 2, 3], fn x -> x > 2 && x * 10 end)   # 30

# with a default:
Enum.find([1, 2, 3], :none, fn x -> x > 5 end)      # :none

# find_index:
Enum.find_index([:a, :b, :c], &(&1 == :b))   # 1

# existence checks (short-circuit):
Enum.any?([1, 2, 3], &(&1 > 2))   # true
Enum.all?([1, 2, 3], &(&1 > 0))   # true

sort 与 sort_by

sort/1 升序;sort/2 可用 :desc 或比较器。sort_by/2 为每个元素提取键再排序——对结构化数据比自定义比较器更清爽。min/max 对空列表报错。

elixir
Enum.sort([3, 1, 2])                # [1, 2, 3]
Enum.sort([3, 1, 2], :desc)         # [3, 2, 1]

# custom comparator:
Enum.sort(["aa", "b", "ccc"], fn a, b -> String.length(a) <= String.length(b) end)
# ["b", "aa", "ccc"]

# sort_by (extract a key per element, sort by it):
Enum.sort_by([%{n: 3}, %{n: 1}, %{n: 2}], & &1.n)
# [%{n: 1}, %{n: 2}, %{n: 3}]

# min / max / min_max:
Enum.min([3, 1, 2])     # 1
Enum.max([3, 1, 2])     # 3
Enum.min_max([3, 1, 2]) # {1, 3}

group_by 与 count

group_by/2 按键函数将元素分桶到映射中。count/1 统计全部,count/2 统计匹配数。chunk_every 按大小切分,chunk_by 按变化的键切分。uniq/1 去重(保留顺序)。

elixir
# group_by buckets elements by a key function:
Enum.group_by([1, 2, 3, 4, 5, 6], fn x -> rem(x, 3) end)
# %{0 => [3, 6], 1 => [1, 4], 2 => [2, 5]}

# group structs by a field:
Enum.group_by([%{t: :a}, %{t: :b}, %{t: :a}], & &1.t)

# count:
Enum.count([1, 2, 3])                          # 3
Enum.count([1, 2, 3, 4], fn x -> x > 2 end)    # 2

# chunk (split into groups):
Enum.chunk_every([1, 2, 3, 4, 5], 2)   # [[1, 2], [3, 4], [5]]
Enum.chunk_by([1, 1, 2, 2, 3], & &1)   # [[1, 1], [2, 2], [3]]

# uniq:
Enum.uniq([1, 1, 2, 3, 3])   # [1, 2, 3]
11

惰性序列

流是惰性可枚举对象:map/filter 等组合成管道,只在被消费时(如被 Enum)才执行。这避免了中间列表——对大型或无限序列很有用。

elixir
# Stream is lazy—operations compose, nothing runs until consumed:
stream = Stream.map([1, 2, 3], fn x -> x * 2 end)
# #Stream<[enum: 1..3, funs: [...]]>  (no computation yet)

# consume to force evaluation:
Enum.to_list(stream)    # [2, 4, 6]
Enum.take(stream, 2)    # [2, 4]

# pipeline of lazy ops:
[1, 2, 3]
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 2))
|> Enum.to_list()       # [4, 6]

无限流

流可以是无限的,因为是惰性的——take/2 只强制求值所需元素。iterate、repeatedly 和 unfold 都生成无限流。unfold 最通用(在元素间携带状态)。

elixir
# Stream.iterate(start, next) - infinite:
Stream.iterate(1, &(&1 + 1))
|> Enum.take(5)   # [1, 2, 3, 4, 5]

# Stream.repeatedly(fun) - infinite calls:
Stream.repeatedly(fn -> :rand.uniform(10) end)
|> Enum.take(3)   # e.g. [4, 7, 2]

# Stream.unfold(state, fun) - stateful infinite:
Stream.unfold(0, fn n -> {n, n + 1} end)
|> Enum.take(4)   # [0, 1, 2, 3]

# fibonacci:
Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
|> Enum.take(8)   # [0, 1, 1, 2, 3, 5, 8, 13]

循环流

Stream.cycle/1 无限重复一个可枚举对象。非常适合把有限列表和重复模式 zip 起来。忘记对无限流调用 take/2 会造成无限循环。

elixir
# Stream.cycle repeats an enumerable forever:
Stream.cycle([:a, :b, :c])
|> Enum.take(7)   # [:a, :b, :c, :a, :b, :c, :a]

# alternate a pattern:
Stream.cycle([true, false])
|> Enum.take(5)   # [true, false, true, false, true]

# zip a finite list with a repeating pattern:
Enum.zip([1, 2, 3, 4, 5], Stream.cycle([:x, :y]))
# [{1, :x}, {2, :y}, {3, :x}, {4, :y}, {5, :x}]

资源流

Stream.resource/3 把外部资源(文件、套接字、数据库游标)包装成惰性流:打开一次,按需产出元素,消费或停止时关闭。适合内存高效的文件处理。

elixir
# Stream.resource(open, next, close) manages external resources:
stream = Stream.resource(
  fn -> File.open!("data.txt") end,           # open once
  fn file ->
    case IO.read(file, :line) do
      :eof -> {:halt, file}
      line -> {[line], file}
    end
  end,
  fn file -> File.close(file) end             # close always
)

# consume line by line - the file closes when done:
stream |> Enum.take(3)

Stream 与 Enum

Enum 是急切的,会创建中间集合;Stream 是惰性的,把操作融合成单趟。大数据集或会短路(take、find)的管道用 Stream。小数据用 Enum 更简单,通常也更快。

elixir
# Enum: eager, returns a new collection, runs immediately:
1..1_000_000 |> Enum.map(&(&1 * 2)) |> Enum.filter(&(&1 > 2))
# builds two large intermediate lists

# Stream: lazy, composes, runs once when consumed:
1..1_000_000
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 2))
|> Enum.take(5)   # only computes until 5 are found

# rule of thumb:
# - small/finite data, need it now      -> Enum
# - huge/infinite data, or early exit   -> Stream
12

函数

匿名函数

匿名函数用 fn ... end 定义,调用时带点号:fun.(args)。&(&1 * &1) 是捕获简写(&1、&2 是位置参数)。用 &Mod.fun/arity 捕获命名函数。支持多子句模式匹配。

elixir
# define with fn ... end:
add = fn a, b -> a + b end
add.(1, 2)   # 3

# shorthand capture:
square = &(&1 * &1)
square.(5)   # 25

# capture a named function:
upcase = &String.upcase/1
upcase.("hi")   # "HI"

# multi-clause anonymous functions:
greet = fn
  :morning -> "Good morning"
  :evening -> "Good evening"
  _        -> "Hello"
end
greet.(:morning)   # "Good morning"

命名函数

def 定义公有、defp 定义私有命名函数。带卫语句(when)的多子句按模式分派。无匹配子句时抛 FunctionClauseError。do: 单行语法常用于简短函数体。

elixir
defmodule Math do
  # public:
  def square(x), do: x * x

  # private (only callable within the module):
  defp secret, do: 42

  # multi-clause with guards:
  def sign(n) when n > 0, do: 1
  def sign(n) when n < 0, do: -1
  def sign(0), do: 0
end

Math.square(4)   # 16
Math.sign(-3)    # -1
# Math.secret()  # UndefinedFunctionError (private)

函数捕获

&Mod.fun/arity 把命名函数捕获为匿名函数。&(&1 ...) 用捕获简写构造 lambda。+/2 等运算符也是函数,可捕获或传给 reduce。

elixir
# capture a named function by module/name/arity:
upcase = &String.upcase/1
upcase.("hi")   # "HI"

# capture a local function:
defmodule M do
  def double(x), do: x * 2
  def quad(x), do: double(x) |> double()
end

# partial application with the capture shorthand:
rem_10 = &rem(&1, 10)
rem_10.(23)   # 3

# operators are functions too:
add = &+/2
add.(3, 4)    # 7

默认参数

\\ 为参数设置默认值,每次调用时求值。默认值在内部生成多个子句头,可能与显式多子句函数微妙交互——只在头部子句中定义默认值。

elixir
defmodule Greet do
  # \\ sets a default value:
  def hello(name, greeting \\ "Hello", punctuation \\ "!") do
    "#{greeting}, #{name}#{punctuation}"
  end
end

Greet.hello("Alice")                 # "Hello, Alice!"
Greet.hello("Alice", "Hi")           # "Hi, Alice!"
Greet.hello("Alice", "Hey", "?")     # "Hey, Alice?"

# defaults interact with multi-clause functions:
# define defaults only in a header clause (no body),
# then write explicit clauses for each arity.

多子句函数

一个命名函数可有多个子句(各有自己的模式/卫语句);第一个匹配者获胜。子句从最具体到最一般排序。最后一个通常是兜底,以避免 FunctionClauseError。

elixir
defmodule FizzBuzz do
  def fb(n) when rem(n, 15) == 0, do: "FizzBuzz"
  def fb(n) when rem(n, 3) == 0,  do: "Fizz"
  def fb(n) when rem(n, 5) == 0,  do: "Buzz"
  def fb(n), do: to_string(n)
end

Enum.map(1..5, &FizzBuzz.fb/1)
# ["1", "2", "Fizz", "4", "Buzz"]

# clause order matters - first match wins, so put
# specific clauses before the general catch-all.

闭包

匿名函数闭包其作用域中的变量,在定义时按值捕获。之后重新绑定变量不影响闭包。要共享可变状态,请使用进程(见“进程”章节)。

elixir
# anonymous functions capture variables from their defining scope:
x = 10
f = fn -> x end
f.()   # 10

# captured by VALUE at definition time:
x = 99
f.()   # still 10 (the old binding)

# closures cannot share mutable state (data is immutable);
# to share state, use a process:
defmodule Counter do
  def start, do: spawn(fn -> loop(0) end)
  defp loop(n) do
    receive do
      {:inc, pid} -> send(pid, {:count, n + 1}); loop(n + 1)
    end
  end
end
13

管道运算符

基础管道

|> 把左侧的值作为第一个参数传给右侧的函数调用。让数据转换从上到下、从左到右阅读,符合你对管道的思考方式。

elixir
# |> passes the left value as the FIRST argument to the right call:
"hello" |> String.upcase()    # "HELLO"
# equivalent to: String.upcase("hello")

[1, 2, 3] |> Enum.map(&(&1 * 2))   # [2, 4, 6]
# equivalent to: Enum.map([1, 2, 3], &(&1 * 2))

5 |> Integer.to_string()      # "5"
:ok |> inspect()              # ":ok"

多步管道

链式 |> 读起来很自然:每步转换上一步的结果。嵌套调用的版本要从内向外读,难跟得多。管道是多步转换的 Elixir 惯用风格。

elixir
"1,2,3,4,5"
|> String.split(",")
|> Enum.map(&String.to_integer/1)
|> Enum.filter(&(&1 > 2))
|> Enum.sum()
# 12  (3 + 4 + 5)

# without pipes (harder to read, nested inside-out):
Enum.sum(
  Enum.filter(
    Enum.map(String.split("1,2,3,4,5", ","), &String.to_integer/1),
    &(&1 > 2)
  )
)

处理元组

管道对任意值都适用,包括元组。某步可能失败时用 case 分支。tap/2(Kernel)执行副作用函数并返回原值——适合管道中间日志。

elixir
# pipe into functions returning tuples, then destructure:
{:ok, content} =
  "config.json"
  |> File.read!()
  |> Jason.decode!()

# use case when a step might fail:
"file.txt"
|> File.read()
|> case do
  {:ok, body}    -> body
  {:error, _}    -> "default"
end

# tap/2 runs a side effect without breaking the pipe:
[1, 2, 3]
|> Enum.map(&(&1 * 2))
|> tap(&IO.inspect(&1, label: "doubled"))
|> Enum.sum()

管道与匿名函数

管道中的匿名函数需显式 .() 调用,看起来别扭——优先用命名函数或捕获。then/2(1.12+)对管道值应用函数,适合内联转换。

elixir
# an anonymous function in a pipe must be called with a dot:
[1, 2, 3]
|> (fn list -> Enum.reverse(list) end).()
# [3, 2, 1]

# cleaner: use a named function or capture:
[1, 2, 3] |> Enum.reverse()   # [3, 2, 1]

# then/2 (Elixir 1.12+) applies a function to the piped value:
5 |> then(fn x -> x * x end)   # 25

# useful for inline construction:
def greet(name) do
  name |> then(&"Hi, #{&1}!")
end

最佳实践

把自己写的函数设计成数据优先(值作为第一个参数)以便于管道。管道长度保持可读;提取命名函数以便复用和测试。副作用用 tap/2,不要随意打印。

elixir
# DO: one operation per step, readable top-to-bottom:
users
|> Enum.filter(& &1.active)
|> Enum.map(& &1.name)
|> Enum.sort()

# DO: extract long pipelines into named functions for reuse/tests:
def active_names(users) do
  users |> Enum.filter(& &1.active) |> Enum.map(& &1.name)
end

# AVOID: more than ~5 steps in one pipe - split or name intermediates.
# AVOID: mixing side effects into a transform pipeline - use tap/2.
# AVOID: piping into functions whose first arg isn't the data -
#        restructure with a small wrapper.

# design your own functions data-first so they pipe well.
14

模块

defmodule

defmodule 聚合命名函数。模块名是原子(Greeter == :"Elixir.Greeter")。嵌套模块形成层次(MyApp.Net.HTTP)。@moduledoc 为模块写文档;在 IEx 用 h Greeter 查看。

elixir
defmodule Greeter do
  @moduledoc "A simple greeting module."

  def hello(name), do: "Hello, #{name}!"
  def goodbye(name), do: "Bye, #{name}!"
end

Greeter.hello("Alice")   # "Hello, Alice!"

# nested modules form a hierarchy:
defmodule MyApp.Net.HTTP do
  def get(url), do: "GET #{url}"
end
MyApp.Net.HTTP.get("/users")

def 与 defp

def 定义公有(导出)函数;defp 定义模块内可用的私有辅助。两者都支持 do: 单行和 do/end 块形式。私有函数有助于封装,且编译器会内联。

elixir
defmodule Account do
  # public API:
  def balance(user), do: user.balance

  # private helper (not exported):
  defp normalize(name), do: String.downcase(name)

  # one-liner and do/end forms:
  def create(name) do
    %{name: normalize(name), balance: 0}
  end
end

Account.balance(%{balance: 100})   # 100
# Account.normalize("ALICE")       # undefined (private)

import

import 把另一模块的函数/宏引入本地作用域,可省略前缀调用。用 only/except 避免污染命名空间。对常用辅助函数,优先 import 而非全限定名。

elixir
# bring a module's functions into local scope (no prefix):
defmodule Colors do
  import String
  def shout(s), do: upcase(s) <> "!"   # calls String.upcase
end

# only/except to narrow what's imported:
import Enum, only: [map: 2, filter: 2]
import String, except: [split: 2]

# import only functions or only macros:
import SomeModule, only: :functions
import SomeMacroModule, only: :macros

alias

alias 为模块取一个较短的本地名(默认取最后一段)。用 :as 自定义名字,用 alias MyApp.{A, B} 多别名形式分组。别名是词法作用域——只在声明它的模块中可见。

elixir
# alias creates a short name for a module:
defmodule MyModule do
  alias MyApp.Very.Long.Path.HTTPClient

  def fetch(url) do
    HTTPClient.get(url)   # instead of MyApp.Very.Long.Path.HTTPClient.get
  end
end

# custom alias name with :as:
alias MyApp.Net.HTTPClient, as: HTTP

# multiple aliases at once:
alias MyApp.{Repo, Schema.User}

require

require 用于调用某模块的宏(在编译期展开)。函数不需要 require。Logger.info/1 是宏,因此使用 Logger 的模块必须先 require Logger。

elixir
# require lets you use a module's MACROS (not needed for functions):
defmodule M do
  require Integer

  def even?(n) do
    if Integer.is_even(n), do: :yes, else: :no   # is_even is a macro
  end
end

# Logger is a macro module, so require it first:
require Logger
Logger.info("starting up")

# Kernel is auto-required; its macros (if, unless, def) are always usable.

use

use Module 在编译期调用 Module 的 __using__/1 宏,可注入任意代码(回调、默认值、require)。它是灵活的扩展点——GenServer、Phoenix、Ecto 等框架都基于它。

elixir
# use invokes the module's __using__/1 macro - a hook for setup:
defmodule MyServer do
  use GenServer          # injects callbacks & defaults
end

# implementing __using__/1 in your own module:
defmodule Tracer do
  defmacro __using__(_opts) do
    quote do
      def traced_call(x), do: IO.inspect(x)
    end
  end
end

defmodule Demo do
  use Tracer
end
Demo.traced_call(42)   # prints 42

# always check the docs to see what use injects.
15

模块属性

作为常量

以 @ 开头的模块属性充当编译期常量。在模块编译时求值,并在每次使用处内联。多次读取属性使用的是模块中该位置当前的值。

elixir
defmodule AppConfig do
  @max_retries 5
  @timeout_ms 5000
  @env Mix.env()    # evaluated at compile time

  def retry_count, do: @max_retries
  def timeout, do: @timeout_ms

  def call do
    if @env == :prod, do: :prod_call, else: :dev_call
  end
end

@moduledoc 与 @doc

@moduledoc 为模块写文档,@doc 为紧随其后的函数/宏写文档。两者都由 IEx 的 h 和 ExDoc 展示。@doc false 把函数从文档中隐藏。@spec 添加类型签名(Dialyzer 和 ExDoc 使用)。

elixir
defmodule Greeter do
  @moduledoc """
  Greets people.

  Use `hello/1` to say hello.
  """

  @doc "Says hello to `name`."
  @spec hello(String.t()) :: String.t()
  def hello(name), do: "Hello, #{name}!"

  @doc false   # hide from docs
  def helper, do: :ok
end

# in IEx: h Greeter, h Greeter.hello

累积属性

如果在重新赋值前先读取属性,属性就会累积值(每次注册都前插到之前的列表)。许多框架(Plug、Phoenix 路由、Ecto schema)就是这样收集声明的。

elixir
defmodule Plugin do
  @callbacks []

  # accumulate by re-reading and prepending:
  defmacro register(name) do
    quote do
      @callbacks [unquote(name) | @callbacks]
    end
  end

  register(:init)
  register(:start)

  def all, do: @callbacks
end

Plugin.all()   # [:start, :init]

@behaviour

@behaviour Module 声明本模块实现了另一模块的 @callback 规范(接口)。@impl true 标记函数实现了某回调,让编译器检查参数数/返回类型。缺失回调会触发编译期警告。

elixir
# a behaviour defines callbacks other modules should implement:
defmodule Parser do
  @doc "Parses input into a data structure."
  @callback parse(String.t()) :: {:ok, term()} | {:error, term()}
end

defmodule JSONParser do
  @behaviour Parser

  @impl true
  def parse(str), do: Jason.decode(str)
end

# if a callback is missing, the compiler warns.

@impl 与 @callback

@impl true(或 @impl GenServer)把函数标记为回调实现,让编译器验证它匹配已知回调并对拼写错误发出警告。用 @callback 声明接口;用 @impl 履行它。

elixir
defmodule MyGenServer do
  use GenServer

  # @impl marks callback implementations:
  @impl true
  def init(state), do: {:ok, state}

  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}

  # without @impl, you get a warning if the function
  # doesn't override a known callback.
end

# defining your own callbacks:
defmodule Sorter do
  @callback compare(a :: term(), b :: term()) :: boolean()
end
16

递归

基本递归

Elixir 没有循环——递归才是迭代方式。定义基线情形(空列表)和递归情形(处理 head 并对 tail 递归)。这正好映射了列表的结构。

elixir
defmodule MyList do
  # base case + recursive case:
  def sum([]), do: 0
  def sum([head | tail]), do: head + sum(tail)

  def len([]), do: 0
  def len([_ | tail]), do: 1 + len(tail)
end

MyList.sum([1, 2, 3])    # 6
MyList.len([:a, :b])     # 2

# recursion replaces loops; pattern matching handles the base case.

尾递归

尾递归函数把递归调用作为最后一步操作,BEAM 因此做尾调用优化(栈恒定)。用累加器推迟计算。非尾递归(head + recurse)会让栈增长。

elixir
# tail-recursive: the recursive call is the LAST operation,
# so BEAM reuses the stack frame (no growth):
defmodule MyList do
  def sum(list), do: sum(list, 0)

  # accumulator carries the running total:
  defp sum([], acc), do: acc
  defp sum([head | tail], acc), do: sum(tail, acc + head)
end

MyList.sum([1, 2, 3, 4])   # 10

# contrast: head + sum(tail) is NOT tail-recursive
# (the + waits for the recursive result).

累加器模式

累加器模式把运行结果穿在递归调用中,从而实现尾递归。用头部插入构建列表最后再反转是 O(n) 的,是惯用的尾递归 map。

elixir
defmodule MyList do
  # reverse using an accumulator (tail-recursive):
  def reverse(list), do: reverse(list, [])
  defp reverse([], acc), do: acc
  defp reverse([h | t], acc), do: reverse(t, [h | acc])

  # map with accumulator (built reversed, then reversed back):
  def map(list, fun), do: map(list, fun, []) |> reverse()
  defp map([], _fun, acc), do: acc
  defp map([h | t], fun, acc), do: map(t, fun, [fun.(h) | acc])
end

MyList.reverse([1, 2, 3])   # [3, 2, 1]

树形递归

树形递归同时递归进 head(当它是列表时)和 tail 以处理嵌套结构。List.flatten/1 和 Deep.count_leaves/1 是经典示例。非尾递归,但浅嵌套没问题。

elixir
defmodule Deep do
  # count leaves of a nested list:
  def count_leaves([]), do: 0
  def count_leaves([head | tail]) when is_list(head) do
    count_leaves(head) + count_leaves(tail)
  end
  def count_leaves([_head | tail]), do: 1 + count_leaves(tail)

  # flatten any nesting:
  def flatten([]), do: []
  def flatten([h | t]) when is_list(h), do: flatten(h) ++ flatten(t)
  def flatten([h | t]), do: [h | flatten(t)]
end

Deep.count_leaves([1, [2, [3]], 4])   # 4

递归 map

map 转换 head 并前插到映射后的 tail;filter 可选地包含 head。两者都按序构建结果,无需反转。生产代码请优先用优化的 Enum 函数。

elixir
defmodule MyList do
  # recursive map:
  def map([], _fun), do: []
  def map([head | tail], fun), do: [fun.(head) | map(tail, fun)]

  # recursive filter:
  def filter([], _fun), do: []
  def filter([h | t], fun) do
    if fun.(h), do: [h | filter(t, fun)], else: filter(t, fun)
  end
end

MyList.map([1, 2, 3], &(&1 * 2))                     # [2, 4, 6]
MyList.filter([1, 2, 3, 4], &(rem(&1, 2) == 0))      # [2, 4]

递归 reduce

reduce 把累加器穿在递归调用中——它是尾递归的,能高效处理大列表。这本质上就是 Enum.reduce 的实现方式。累加器持有运行中的结果。

elixir
defmodule MyList do
  # left fold, recursive and tail-recursive:
  def reduce([], acc, _fun), do: acc
  def reduce([h | t], acc, fun), do: reduce(t, fun.(h, acc), fun)
end

MyList.reduce([1, 2, 3, 4], 0, &+/2)   # 10

# the recursive call is the LAST operation,
# so this runs in constant stack space even for huge lists.
# This is essentially how Enum.reduce is implemented.
17

进程

spawn

BEAM 进程很轻量(千字节级;可运行数百万个)。spawn/1 在新进程中运行一个函数。与 OS 线程不同,它们廉价且隔离——不共享状态,靠消息传递通信。

elixir
# spawn/1 creates a lightweight BEAM process:
pid = spawn(fn -> IO.puts("hello from process") end)
# prints "hello from process"; the process is now dead

# spawn/3 with module/function/args:
pid = spawn(Enum, :map, [[1, 2, 3], &(&1 * 2)])

# process info:
Process.alive?(pid)   # false (already finished)
self()                # current process PID

send 与 receive

send/2 把消息放进进程邮箱;receive/1 按模式取出消息。消息排队等匹配。after 子句加超时——after 0 表示非阻塞。每个匹配内邮箱顺序保留。

elixir
# send/2 puts a message in a pid's mailbox:
pid = spawn(fn ->
  receive do
    {:hello, from} -> send(from, {:hi, self()})
    {:bye, _}      -> :ok
  end
end)

send(pid, {:hello, self()})

# receive in the current process:
receive do
  {:hi, pid} -> IO.puts("got hi from #{inspect(pid)}")
after
  1000 -> :timeout
end

进程状态循环

长期运行的进程通过在 receive 循环中递归来保存状态——每个处理器把 loop(new_state) 作为尾调用。GenServer 底层就是这么做的。状态对进程私有;无需锁。

elixir
# a process that keeps state via tail recursion:
defmodule Counter do
  def start(n), do: spawn(fn -> loop(n) end)

  defp loop(state) do
    receive do
      {:inc, by} -> loop(state + by)
      {:get, from} ->
        send(from, {:count, state})
        loop(state)
      :stop -> :ok
    end
  end
end

c = Counter.start(0)
send(c, {:inc, 5})
send(c, {:get, self()})
receive do {:count, n} -> n end   # 5

链接与终止

spawn_link/1 链接两个进程:一方异常退出,另一方也退出(Elixir 的“让它崩溃”哲学)。设 Process.flag(:trap_exit, true) 可把退出转成 {:EXIT, pid, reason} 消息而不跟着退出。

elixir
# spawn_link: linked processes die together (fault tolerance):
pid = spawn_link(fn ->
  receive do
    :boom -> raise "crash"
  end
end)

send(pid, :boom)
# BOTH processes die (the linked one raises, this one exits too)

# trap exits to handle instead of dying:
Process.flag(:trap_exit, true)
spawn_link(fn -> exit(:kaboom) end)

receive do
  {:EXIT, from, reason} -> IO.puts("#{inspect(from)} died: #{reason}")
end

监视器

Process.monitor/1 单向监视一个进程:它死时你会收到 {:DOWN, ref, :process, pid, reason},自己不会死。想观察但不耦合生命周期时用监视器。用 Process.demonitor/1 取消。

elixir
# monitors are one-way and don't kill the watcher:
pid = spawn(fn -> exit(:boom) end)
ref = Process.monitor(pid)

receive do
  {:DOWN, ^ref, :process, ^pid, reason} ->
    IO.puts("monitored process died: #{reason}")
end
# prints "monitored process died: boom"

# unlike links, the current process keeps running.
# monitors are asymmetric and unidirectional.
Process.demonitor(ref)   # stop watching early

超时

receive 的 after 子句设毫秒级超时;after 0 做非阻塞检查(无匹配立即返回)。这能实现轮询、刷新循环和超时。超时对避免永久阻塞至关重要。

elixir
# receive with after for a timeout:
receive do
  {:data, x} -> x
after
  1000 -> :no_response   # 1 second
end

# after 0 = non-blocking check:
receive do
  msg -> {:got, msg}
after
  0 -> :empty
end

# flush helper (drain the mailbox):
def flush do
  receive do
    _ -> flush()
  after
    0 -> :ok
  end
end
18

OTP - GenServer

定义 GenServer

use GenServer 设置 behaviour 并注入默认实现。实现回调:init/1、handle_call/3(同步)、handle_cast/2(异步)、handle_info/2(其他消息)。@impl true 标记每个回调以便编译期检查。

elixir
defmodule Counter do
  use GenServer

  # callback: initial state
  @impl true
  def init(initial), do: {:ok, initial}

  # synchronous (call) - returns a reply
  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}

  # asynchronous (cast) - no reply
  @impl true
  def handle_cast({:inc, n}, state), do: {:noreply, state + n}
end

启动 GenServer

GenServer.start_link/3 启动服务器(调用 init/1)并链接到调用者。用 name: 注册名称,以便用原子而非 PID 调用。真实应用中,请在 Supervisor 下启动服务器,而非直接启动。

elixir
# start_link links the server to the caller (named or unnamed):
{:ok, pid} = GenServer.start_link(Counter, 0, name: MyCounter)

# register via the module name:
{:ok, pid} = GenServer.start_link(Counter, 0, name: __MODULE__)

# starting under a Supervisor (typical in real apps):
children = [
  {Counter, 0}
]
Supervisor.start_link(children, strategy: :one_for_one)

handle_call(同步)

handle_call/3 通过 GenServer.call/3 处理同步请求。from 元组允许你用 {:noreply, state} 稍后用 GenServer.reply(from, reply) 回复。返回 {:stop, ...} 会终止服务器。

elixir
@impl true
def handle_call(:get, _from, state) do
  {:reply, state, state}
end

@impl true
def handle_call({:add, n}, _from, state) do
  new_state = state + n
  {:reply, new_state, new_state}
end

# reply tuples:
#   {:reply, reply, new_state}
#   {:reply, reply, new_state, timeout | :hibernate}
#   {:noreply, new_state}        # reply later with GenServer.reply/2
#   {:stop, reason, reply, new_state}

handle_cast(异步)

handle_cast/2 通过 GenServer.cast/2 处理异步“发后即忘”消息。没有回复——返回 {:noreply, new_state} 继续,或 {:stop, reason, new_state} 终止。调用方不需要结果时用 cast。

elixir
@impl true
def handle_cast({:inc, n}, state) do
  {:noreply, state + n}
end

@impl true
def handle_cast(:reset, _state) do
  {:noreply, 0}
end

# cast returns immediately - the caller doesn't wait for a reply.
# useful for fire-and-forget updates, logging, side effects.

# return tuples:
#   {:noreply, new_state}
#   {:noreply, new_state, timeout | :hibernate}
#   {:stop, reason, new_state}

客户端 API

惯例:把客户端 API(调用 GenServer.call/cast 的函数)和服务器回调放在同一模块。调用方用 Counter.get()/inc(),无需关心 call/cast 细节。这把消息传递隐藏在干净的函数接口背后。

elixir
defmodule Counter do
  use GenServer

  # client API (called by other code):
  def start_link(initial) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end
  def get, do: GenServer.call(__MODULE__, :get)
  def inc(n \\ 1), do: GenServer.cast(__MODULE__, {:inc, n})

  # server callbacks:
  @impl true
  def init(initial), do: {:ok, initial}
  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}
  @impl true
  def handle_cast({:inc, n}, state), do: {:noreply, state + n}
end

handle_info

handle_info/2 处理“原始”消息——普通 send、:DOWN 监视通知和 Process.send_after/3 超时。call 和 cast 有各自处理器;其他都落到这里。务必实现它,以免邮箱堆积。

elixir
defmodule Heartbeat do
  use GenServer

  @impl true
  def init(_) do
    send(self(), :tick)
    {:ok, 0}
  end

  # handle non-call/cast messages (plain sends, monitors, timeouts):
  @impl true
  def handle_info(:tick, count) do
    IO.puts("tick ##{count}")
    Process.send_after(self(), :tick, 1000)   # schedule next
    {:noreply, count + 1}
  end

  def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
    IO.puts("#{inspect(pid)} died: #{reason}")
    {:noreply, state}
  end
end
19

Mix 工具

创建项目

mix new 脚手架生成含 lib/、test/、mix.exs 和 .formatter.exs 的项目。--sup 添加带监督树的 Application 模块(用于 OTP 应用)。Umbrella 项目容纳多个共享依赖的子应用。

elixir
# generate a new project:
$ mix new my_app
# creates:
#   my_app/
#     lib/my_app.ex
#     lib/my_app/application.ex
#     test/my_app_test.exs
#     mix.exs
#     .formatter.exs
#     README.md

# with a supervision tree (OTP app):
$ mix new my_app --sup

# inside an umbrella project:
$ mix new apps/child_app

mix.exs 与依赖

mix.exs 定义项目:应用名、版本、依赖和别名。依赖是 {name, version, opts} 元组。only: 限制环境;runtime: false 表示仅编译。mix deps.get 拉取;mix deps.tree 可视化。

elixir
defp deps do
  [
    {:phoenix, "~> 1.7"},
    {:ecto_sql, "~> 3.10"},
    {:jason, "~> 1.4"},
    {:ex_doc, "~> 0.30", only: :dev, runtime: false},
    {:credo, "~> 1.7", only: [:dev, :test], runtime: false}
  ]
end

# install:
$ mix deps.get

# list the dependency tree:
$ mix deps.tree

# why is a dependency included?
$ mix deps.unlock --unused

常用 Mix 命令

常用任务:deps.get 拉取依赖,compile 构建项目,iex -S mix 打开加载了项目和依赖的 IEx,test 运行 ExUnit,format 自动格式化。mix help 列出全部任务;mix help TASK 查看详情。

elixir
$ mix new app        # scaffold a project
$ mix deps.get       # fetch dependencies
$ mix deps.compile   # compile dependencies
$ mix compile        # compile the project
$ mix run            # start the app and keep it running
$ mix run -e "MyApp.hello()"   # run an expression
$ iex -S mix         # IEx with the project loaded
$ mix test           # run the test suite
$ mix format         # format source files
$ mix phx.server     # (Phoenix) start the web server
$ mix help           # list all available tasks

Mix 任务

自定义任务位于 Mix.Tasks.* 模块并使用 Mix.Task。模块名(Mix.Tasks. 之后的部分)成为命令(点号变下划线)。@shortdoc 显示在 mix help 中。Mix.shell() 提供用户 I/O。

elixir
# define a custom task module:
defmodule Mix.Tasks.MyApp.Hello do
  use Mix.Task

  @shortdoc "Says hello"

  def run(_args) do
    Mix.shell().info("Hello from my task!")
  end
end

# run it:
$ mix my_app.hello
# Hello from my task!

环境

Mix 有三个环境: :dev(默认)、:test、:prod。Mix.env() 返回当前环境。config/{env}.exs 按环境覆盖 config/config.exs。构建时设置 MIX_ENV(如 MIX_ENV=prod mix phx.server)。

elixir
# mix.exs uses Mix.env() to branch per environment:
def project do
  [
    app: :my_app,
    version: "0.1.0",
    elixir: "~> 1.15",
    start_permanent: Mix.env() == :prod,   # permanent in prod
    deps: deps()
  ]
end

# config/config.exs:
import Config
config :my_app, key: :default

# config/dev.exs, config/prod.exs, config/test.exs override per env.
# select env at build time:
#   MIX_ENV=prod mix compile
#   MIX_ENV=prod mix phx.server
20

ExUnit 测试

基本测试

use ExUnit.Case 引入 test/2、assert/1 等。async: true 让测试模块与其他模块并发运行(共享全局状态的测试避免用)。assert_raise/2 捕获预期异常。用 mix test 运行。

elixir
# test/my_app_test.exs
defmodule MyAppTest do
  use ExUnit.Case, async: true

  test "addition" do
    assert 1 + 1 == 2
  end

  test "lists have a head" do
    [head | _] = [1, 2, 3]
    assert head == 1
  end

  test "raises on missing key" do
    assert_raise KeyError, fn -> %{a: 1}.b end
  end
end

# run: mix test

describe 分组

describe 把相关测试归到共享前缀下,提升可读性和测试输出。每个 describe 块可以有自己的 setup。describe 内的测试彼此通常不同步,但与其他模块并发。

elixir
defmodule UserTest do
  use ExUnit.Case

  describe "name validation" do
    test "requires a name" do
      assert User.new(%{}) |> valid?() == false
    end

    test "accepts a non-empty name" do
      assert User.new(%{name: "A"}) |> valid?() == true
    end
  end

  describe "age" do
    test "must be positive" do
      assert {:error, _} = User.new(%{age: -1})
    end
  end
end

setup 与 setup_all

setup 在每个测试前运行,setup_all 每模块运行一次。两者都可返回 {:ok, key: val} 添加到测试上下文,再在测试签名中模式匹配。on_exit/1 注册的清理即使失败也总会运行。

elixir
defmodule DbTest do
  use ExUnit.Case, async: false

  # runs once before each test; context is merged into the test:
  setup do
    conn = open_conn()
    on_exit(fn -> close_conn(conn) end)
    {:ok, conn: conn}
  end

  # runs once for the whole module:
  setup_all do
    {:ok, schema: create_schema()}
  end

  test "uses conn", %{conn: conn} do
    assert query(conn, "SELECT 1") == 1
  end
end

断言

assert 检查参数是否为 truthy(失败时有很好的错误输出)。refute 是其反操作。assert_in_delta 在容差内比较浮点数。模式断言(assert {:ok, v} = ...)既检查又绑定。

elixir
assert 1 + 1 == 2
refute 1 == 2                          # opposite of assert
assert_in_delta 3.14, 3.1415, 0.01     # floats within tolerance
assert_raise ArgumentError, fn -> hd([]) end

# pattern assertions (both check and bind):
assert {:ok, val} = parse("1")
assert %User{name: "A"} = build_user()

# catch exits/throws:
assert catch_exit(exit(:boom)) == :boom

# enum/map assertions:
assert length([1, 2]) == 2
assert user.age > 18

异步测试

async: true 让测试模块与其他模块并发——测试不共享可变状态时安全。start_supervised/1 启动与测试生命周期绑定的进程并自动清理。默认用 async: true;仅在需要时关闭。

elixir
defmodule CounterTest do
  use ExUnit.Case, async: true   # concurrent with other modules

  # each test gets its own Counter process, so no shared state:
  setup do
    {:ok, pid} = start_supervised(Counter)
    %{pid: pid}
  end

  test "increments", %{pid: pid} do
    Counter.inc(pid)
    assert Counter.get(pid) == 1
  end

  test "starts at zero", %{pid: pid} do
    assert Counter.get(pid) == 0
  end
end

这篇内容对您有帮助吗?