入门
基础与 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默认参数
\\ 为参数设置默认值,每次调用时求值。默认值在内部生成多个子句头,可能与显式多子句函数微妙交互——只在头部子句中定义默认值。