入门
基础与 IEx
Elixir 运行在 Erlang VM (BEAM) 上,以并发和容错性著称。原子是常量,其名称即为其值。<> 连接字符串。元组大小固定,列表是链表。
# 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 是调试利器。
# 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 项目。
# 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 打印类型定义。这些是交互式学习的重要工具。
# 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),便于查找。
# 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基本类型
原子
原子是不可变常量,其名称即为其值。true、false 和 nil 都是原子。不要从不可信输入创建原子——原子表不会被垃圾回收,可能造成内存泄漏。
# 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) 字面量。
# 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 函数期望二进制。
# 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 要求布尔值;&&/||/! 接受任意值并返回操作数本身。
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} 元组的列表;映射允许任意键。
# 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}运算符
算术运算符
/ 始终返回浮点数;用 div/2 进行整数除法。rem/2 取被除数的符号,Integer.mod/2 始终非负。Integer.pow/2(1.12+)做整数幂运算。
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),因此任意两个值都能比较且不会报错。
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,并返回操作数本身而非布尔值。
# 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)字符串连接
<> 连接二进制(字符串);++ 连接列表(包括字符列表)。-- 移除右侧每个元素的第一个匹配。可读性上优先用插值而非反复 <>。
"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。变量可重新绑定(指向新值),但底层数据是不可变的。
# = 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。|> 是管道运算符(见专章)。
# 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"模式匹配
匹配运算符
= 将模式中的变量绑定到右侧的值。在同一模式中重复使用某变量会强制这些值相等。_ 匹配(并丢弃)任意值。
# = 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 子句和函数头中按已有值匹配时不可或缺。
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 可用独立子句分别处理每种形状。
{: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] 将列表分解为首元素和剩余部分。列表按结构匹配——递归遍历就用它。空列表 [] 只匹配空列表。
# 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 匹配动态键。结构体按名称和字段匹配。
# 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)。
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不可变性
不可变数据
所有 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),需遍历左列表。原列表被共享而非复制——高效且安全。
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 仅在键不存在时添加。这些都返回新映射;原映射不变。映射内部共享结构以提升效率。
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。
# 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))。不可变性让跨进程共享无需锁或复制即可安全进行。
# 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)列表
头部与尾部
列表是单向链表:head 是首元素,tail 是剩余部分(一个列表)。hd/1 和 tl/1 提取它们(空列表报错)。头部插入 O(1);length/1 是 O(n) 因为要遍历。
[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 模块。
[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: 改变目标容器(映射、字符串等)。在一个表达式中实现转换和过滤很强大。
# 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。
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 配对两个列表的元素。
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) 内高效构建反序列表。
# 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]元组
创建元组
元组在连续内存中存放固定数量的元素(按下标 O(1) 访问)。用于小型、固定形状的聚合——尤其是 {:ok, value} 等标签元组——而非可增长的集合。
{: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 返回新元组(不可变)。模式匹配比按下标访问更地 道——更清晰且自解释。
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 都能解构它们。务必同时处理两个分支。
# 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),因为整个元组要复制。如果频繁增删元素,改用列表或映射。
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)。
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关键字列表
创建关键字列表
关键字列表是 {atom, value} 元组的列表,有特殊的字面语法。键必须是原子,且允许重复。它本质就是列表,因此所有列表操作都适用(访问是 O(n))。
# 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)——频繁访问请用映射。
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)。
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]关键字列表与映射
关键字列表用于有序、原子键的选项列表(如函数选项)。映射用于任意键类型和快速查找的通用键值存储。不要把关键字列表当作主数据结构。
# 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)。配置文件大量使用。
# 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)映射
创建映射
映射存放任意键类型的键值对。%{key: val} 语法要求原子键;其他类型用 key => val。map_size/1 是 O(1)。映射是主要的键值数据结构。
# 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,便于显式处理。
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 合并(冲突时右侧优先)。都返回新映射;原映射不变。
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 能优雅地导航和修改嵌套结构。
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 响应和配置。
# 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__)并在编译期强制键——用于领域实体。
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"Enum
map 与 each
map/2 将每个元素转换为新列表(长度相同)。each/2 用于副作用,返回 :ok。Enum 适用于任何可枚举对象:列表、映射(以 {k,v} 形式)、范围等。
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 配合条件列表。
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 不带初值时使用第一个元素(空列表报错)。
# 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) # 6find 与 find_value
find 返回第一个匹配的元素;find_value 返回函数的 truthy 结果(配合哨兵值做转换很方便)。any?/all? 一旦得到答案就短路,不必扫描整个列表。
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)) # truesort 与 sort_by
sort/1 升序;sort/2 可用 :desc 或比较器。sort_by/2 为每个元素提取键再排序——对结构化数据比自定义比较器更清爽。min/max 对空列表报错。
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 去重(保留顺序)。
# 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]流
惰性序列
流是惰性可枚举对象:map/filter 等组合成管道,只在被消费时(如被 Enum)才执行。这避免了中间列表——对大型或无限序列很有用。
# 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 最通用(在元素间携带状态)。
# 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 会造成无限循环。
# 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 把外部资源(文件、套接字、数据库游标)包装成惰性流:打开一次,按需产出元素,消费或停止时关闭。适合内存高效的文件处理。
# 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 更简单,通常也更快。
# 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函数
匿名函数
匿名函数用 fn ... end 定义,调用时带点号:fun.(args)。&(&1 * &1) 是捕获简写(&1、&2 是位置参数)。用 &Mod.fun/arity 捕获命名函数。支持多子句模式匹配。
# 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: 单行语法常用于简短函数体。
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。
# 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默认参数
\\ 为参数设置默认值,每次调用时求值。默认值在内部生成多个子句头,可能与显式多子句函数微妙交互——只在头部子句中定义默认值。
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。
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.闭包
匿名函数闭包其作用域中的变量,在定义时按值捕获。之后重新绑定变量不影响闭包。要共享可变状态,请使用 进程(见“进程”章节)。
# 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管道运算符
基础管道
|> 把左侧的值作为第一个参数传给右侧的函数调用。让数据转换从上到下、从左到右阅读,符合你对管道的思考方式。
# |> 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 惯用风格。
"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)执行副作用函数并返回原值——适合管道中间日志。
# 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+)对管道值应用函数,适合内联转换。
# 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,不要随意打印。
# 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.模块
defmodule
defmodule 聚合命名函数。模块名是原子(Greeter == :"Elixir.Greeter")。嵌套模块形成层次(MyApp.Net.HTTP)。@moduledoc 为模块写文档;在 IEx 用 h Greeter 查看。
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 块形式。私有函数有助于封装,且编译器会内联。
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 而非全限定名。
# 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: :macrosalias
alias 为模块取一个较短的本地名(默认取最后一段)。用 :as 自定义名字,用 alias MyApp.{A, B} 多别名形式分组。别名是词法作用域——只在声明它的模块中可见。
# 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。
# 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 等框架都基于它。
# 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.模块属性
作为常量
以 @ 开头的模块属性充当编译期常量。在模块编译时求值,并在每次使用处内联。多次读取属性使用的是模块中该位置当前的值。
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 使用)。
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)就是这样收集声明的。
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 标记函数实现了某回调,让编译器检查参数数/返回类型。缺失回调会触发编译期警告。
# 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 履行它。
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递归
基本递归
Elixir 没有循环——递归才是迭代方式。定义基线情形(空列表)和递归情形(处理 head 并对 tail 递归)。这正好映射了列表的结构。
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)会让栈增长。
# 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。
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 是经典示例。非尾递归,但浅嵌套没问题。
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 函数。
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 的实现方式。累加器持有运行中的结果。
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.进程
spawn
BEAM 进程很轻量(千字节级;可运行数百万个)。spawn/1 在新进程中运行一个函数。与 OS 线程不同,它们廉价且隔离— —不共享状态,靠消息传递通信。
# 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 PIDsend 与 receive
send/2 把消息放进进程邮箱;receive/1 按模式取出消息。消息排队等匹配。after 子句加超时——after 0 表示非阻塞。每个匹配内邮箱顺序保留。
# 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 底层就是这么做的。状态对进程私有;无需锁。
# 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} 消息而不跟着退出。
# 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 取消。
# 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 做非阻塞检查(无匹配立即返回)。这能实现轮询、刷新循环和超时。超时对避免永久阻塞至关重要。
# 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
endOTP - GenServer
定义 GenServer
use GenServer 设置 behaviour 并注入默认实现。实现回调:init/1、handle_call/3(同步)、handle_cast/2(异步)、handle_info/2(其他消息)。@impl true 标记每个回调以便编译期检查。
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 下启动服务器,而非直接启动。
# 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, ...} 会终止服务器。
@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。
@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 细节。这把消息传递隐藏在干净的函数接口背后。
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}
endhandle_info
handle_info/2 处理“原始”消息——普通 send、:DOWN 监视通知和 Process.send_after/3 超时。call 和 cast 有各自处理器;其他都落到这里。务必实现它,以免邮箱堆积。
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
endMix 工具
创建项目
mix new 脚手架生成含 lib/、test/、mix.exs 和 .formatter.exs 的项目。--sup 添加带监督树的 Application 模块(用于 OTP 应用)。Umbrella 项目容纳多个共享依赖的子应用。
# 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_appmix.exs 与依赖
mix.exs 定义项目:应用名、版本、依赖和别名。依赖是 {name, version, opts} 元组。only: 限制环境;runtime: false 表示仅编译。mix deps.get 拉取;mix deps.tree 可视化。
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 查看详情。
$ 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 tasksMix 任务
自定义任务位于 Mix.Tasks.* 模块并使用 Mix.Task。模块名(Mix.Tasks. 之后的部分)成为命令(点号变下划线)。@shortdoc 显示在 mix help 中。Mix.shell() 提供用户 I/O。
# 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)。
# 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.serverExUnit 测试
基本测试
use ExUnit.Case 引入 test/2、assert/1 等。async: true 让测试模块与其他模块并发运行(共享全局状态的测试避免用)。assert_raise/2 捕获预期异常。用 mix test 运行。
# 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 testdescribe 分组
describe 把相关测试归到共享前缀下,提升可读性和测试输出。每个 describe 块可以有自己的 setup。describe 内的测试彼此通常不同步,但与其他模块并发。
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
endsetup 与 setup_all
setup 在每个测试前运行,setup_all 每模块运行一次。两者都可返回 {:ok, key: val} 添加到测试上下文,再在测试签名中模式匹配。on_exit/1 注册的清理即使失败也总会运行。
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} = ...)既检查又绑定。
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;仅在需要时关闭。
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相关 Elixir 代码片段
Copy-paste ready code for common tasks.
这篇内容对您有帮助吗?