Elixir 函数式编程:Enum 模块
Elixir 是一种基于 Erlang 虚拟机的函数式编程语言,以其强大的并发能力和优雅的语法著称。在 Elixir 的标准库中,Enum 模块是最常用、最核心的模块之一,它提供了一套完整的枚举操作函数,让你能够以声明式的方式处理集合数据。
掌握 Enum 模块是写出地道 Elixir 代码的第一步。本文将系统讲解 Enum 模块的核心函数、常见用法以及最佳实践。
什么是 Enum 模块
Enum 模块位于 Elixir 命名空间下,专门用于对可枚举对象(enumerables)进行操作。在 Elixir 中,列表(List)、映射(Map)、区间(Range)、字符串(String)等类型都是可枚举对象。
# 这些都是可枚举对象
[1, 2, 3, 4, 5] # 列表
%{a: 1, b: 2} # 映射
1..10 # 区间(从 1 到 10)
"hello" # 字符串(Unicode 码点列表)
Enum 模块提供了超过 80 个函数,涵盖了遍历、过滤、转换、聚合等几乎所有常见的集合操作需求。这些函数都遵循函数式编程的核心理念:不可变性和链式调用。
第一类函数:遍历与执行
这一类函数用于遍历集合中的每个元素并执行相应操作。
each —— 逐元素处理
each 函数接收一个可枚举对象和一个回调函数,它会遍历每个元素并执行回调,但不返回任何有意义的结果(返回 :ok)。这相当于其他语言中的 forEach 或 for...of 循环。
Enum.each([1, 2, 3, 4], fn x ->
IO.puts("当前数字是: #{x}")
end)
执行结果:
当前数字是: 1
当前数字是: 2
当前数字是: 3
当前数字是: 4
典型应用场景:日志记录、调试输出、触发副作用操作。
map —— 元素转换
map 函数对集合中的每个元素应用给定的转换函数,返回一个新的列表(原集合保持不变)。这是函数式编程中最常用的操作之一。
# 将每个数字翻倍
numbers = [1, 2, 3, 4, 5]
doubled = Enum.map(numbers, fn x -> x * 2 end)
IO.inspect(doubled) # 输出: [2, 4, 6, 8, 10]
# 使用捕获语法更简洁
squared = Enum.map(numbers, &(&1 * &1))
IO.inspect(squared) # 输出: [1, 4, 9, 16, 25]
# 转换字符串列表
names = ["alice", "bob", "charlie"]
capitalized = Enum.map(names, &String.capitalize/1)
IO.inspect(capitalized) # 输出: ["Alice", "Bob", "Charlie"]
关键特点:永远返回一个新列表,不修改原集合。
map_each —— 映射并保留原始结构
当可枚举对象不是列表时(如 Map),map 的行为会有所不同。Map 的 Enum.map 只能遍历值,无法同时保留键:
# 对 Map 使用 Enum.map 只会得到值列表
original = %{a: 1, b: 2, c: 3}
values_only = Enum.map(original, fn {k, v} -> v * 2 end)
IO.inspect(values_only) # 输出: [2, 4, 6],丢失了键信息
对于需要同时保留键的情况,应使用 Map.new/2:
doubled_map = Map.new(original, fn {k, v} -> {k, v * 2} end)
IO.inspect(doubled_map) # 输出: %{a: 2, b: 4, c: 6}
第二类函数:过滤与筛选
这一类函数用于根据条件筛选出符合条件的元素。
filter —— 条件筛选
filter 函数接收一个可枚举对象和一个谓词函数(返回布尔值的函数),返回所有满足条件的元素组成的新列表。
numbers = 1..20
# 筛选偶数
evens = Enum.filter(numbers, fn x -> rem(x, 2) == 0 end)
IO.inspect(evens) # 输出: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# 使用捕获语法筛选大于 10 的数
greater_than_10 = Enum.filter(numbers, &(&1 > 10))
IO.inspect(greater_than_10) # 输出: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
# 从用户列表中筛选活跃用户
users = [
%{name: "Alice", active: true},
%{name: "Bob", active: false},
%{name: "Charlie", active: true}
]
active_users = Enum.filter(users, fn u -> u.active end)
IO.inspect(active_users)
# 输出: [%{name: "Alice", active: true}, %{name: "Charlie", active: true}]
reject —— 反向筛选
reject 是 filter 的对立面,返回所有不满足条件的元素。
numbers = 1..10
# 排除奇数(只保留偶数)
even_only = Enum.reject(numbers, fn x -> rem(x, 2) == 1 end)
IO.inspect(even_only) # 输出: [2, 4, 6, 8, 10]
# 排除 nil 值
mixed_list = [1, nil, 2, nil, 3, 4, nil]
without_nils = Enum.reject(mixed_list, &is_nil/1)
IO.inspect(without_nils) # 输出: [1, 2, 3, 4]
find —— 查找单个元素
当你只需要找到第一个满足条件的元素时,使用 find 函数。它返回该元素,如果没找到则返回默认值(默认为 nil)。
numbers = 1..100
# 找第一个大于 50 的平方数
first_large_square = Enum.find(numbers, fn x ->
root = :math.sqrt(x)
root == trunc(root) and x > 50
end)
IO.inspect(first_large_square) # 输出: 64(8 的平方)
# 自定义默认值
not_found = Enum.find(numbers, :not_found, fn x -> x > 1000 end)
IO.inspect(not_found) # 输出: :not_found
第三类函数:聚合与归约
这一类函数将整个集合归纳为一个单一的值,是函数式编程的精髓所在。
reduce —— 核心聚合函数
reduce 是 Enum 模块中最强大、最灵活的函数。它接收一个可枚举对象、一个初始累加器和一个回调函数,逐个元素处理并累积结果。
# 基本用法:求和
numbers = [1, 2, 3, 4, 5]
sum = Enum.reduce(numbers, 0, fn x, acc -> x + acc end)
IO.inspect(sum) # 输出: 15
# 使用捕获语法(更简洁)
product = Enum.reduce([1, 2, 3, 4], 1, &(&1 * &2))
IO.inspect(product) # 输出: 24
# 统计词频
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
word_count = Enum.reduce(words, %{}, fn word, acc ->
Map.update(acc, word, 1, fn count -> count + 1 end)
end)
IO.inspect(word_count)
# 输出: %{"apple" => 3, "banana" => 2, "orange" => 1}
reduce 的回调函数接收两个参数:当前元素和累加器,返回一个新的累加器。这个模式可以表达任何聚合逻辑。
count —— 计数
count 有两种用法:直接计数集合大小,或根据条件计数。
numbers = 1..100
# 集合大小
total = Enum.count(numbers)
IO.inspect(total) # 输出: 100
# 满足条件的元素数量
even_count = Enum.count(numbers, fn x -> rem(x, 2) == 0 end)
IO.inspect(even_count) # 输出: 50
min / max / sum —— 极值与求和
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
IO.puts("最小值: #{Enum.min(numbers)}") # 输出: 1
IO.puts("最大值: #{Enum.max(numbers)}") # 输出: 9
IO.puts("求和: #{Enum.sum(numbers)}") # 输出: 31
# 处理空列表时的默认值
empty_list = []
IO.puts("空列表最小值: #{Enum.min(empty_list, fn -> 0 end)}") # 输出: 0
第四类函数:分组与拆分
这一类函数用于将集合分割或重组。
chunk_by —— 按条件分组
chunk_by 将相邻的、满足相同条件的元素放入同一组,返回一个由子列表组成的列表。
# 按奇偶性分组(相邻的偶数/奇数会分到一起)
numbers = [1, 2, 3, 5, 7, 9, 10, 11, 13]
grouped = Enum.chunk_by(numbers, fn x -> rem(x, 2) == 0 end)
IO.inspect(grouped)
# 输出: [[1], [2], [3, 5, 7, 9], [10], [11, 13]]
# 按首字母分组
words = ["apple", "apricot", "banana", "berry", "cherry"]
by_letter = Enum.chunk_by(words, &String.first/1)
IO.inspect(by_letter)
# 输出: [["apple", "apricot"], ["banana", "berry"], ["cherry"]]
group_by —— 按键映射分组
group_by 将元素按照某个键进行分组,返回一个映射。
# 按奇偶性分组
numbers = 1..10
by_parity = Enum.group_by(numbers, fn x -> rem(x, 2) end)
IO.inspect(by_parity)
# 输出: %{0 => [2, 4, 6, 8, 10], 1 => [1, 3, 5, 7, 9]}
# 按用户状态分组
users = [
%{name: "Alice", status: :online},
%{name: "Bob", status: :offline},
%{name: "Charlie", status: :online},
%{name: "Diana", status: :offline}
]
by_status = Enum.group_by(users, &(&1.status))
IO.inspect(by_status)
# 输出: %{
# offline: [%{name: "Bob", status: :offline}, %{name: "Diana", status: :offline}],
# online: [%{name: "Alice", status: :online}, %{name: "Charlie", status: :online}]
# }
split —— 分割集合
split 在指定位置将集合分为两部分。
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 从中间分割
{first_half, second_half} = Enum.split(numbers, 5)
IO.inspect(first_half) # 输出: [1, 2, 3, 4, 5]
IO.inspect(second_half) # 输出: [6, 7, 8, 9, 10]
# 按条件分割
{passed, failed} = Enum.split_with(numbers, &(&1 >= 6))
IO.inspect(passed) # 输出: [6, 7, 8, 9, 10]
IO.inspect(failed) # 输出: [1, 2, 3, 4, 5]
take / drop —— 取值与丢弃
numbers = 1..10
# 取前 5 个
first_five = Enum.take(numbers, 5)
IO.inspect(first_five) # 输出: [1, 2, 3, 4, 5]
# 从末尾取 3 个
last_three = Enum.take(numbers, -3)
IO.inspect(last_three) # 输出: [8, 9, 10]
# 跳过前 3 个
skip_first = Enum.drop(numbers, 3)
IO.inspect(skip_first) # 输出: [4, 5, 6, 7, 8, 9, 10]
# 跳过满足条件的元素
drop_while = Enum.drop_while(numbers, &(&1 < 5))
IO.inspect(drop_while) # 输出: [5, 6, 7, 8, 9, 10]
第五类函数:排序与去重
这一类函数用于对集合进行排序和去重处理。
sort —— 排序
numbers = [5, 2, 8, 1, 9, 3]
# 默认升序排序(使用 Erlang 的术语比较器)
ascending = Enum.sort(numbers)
IO.inspect(ascending) # 输出: [1, 2, 3, 5, 8, 9]
# 自定义排序函数(降序)
descending = Enum.sort(numbers, &(&1 > &2))
IO.inspect(descending) # 输出: [9, 8, 5, 3, 2, 1]
# 按绝对值排序
mixed = [3, -1, 4, -2, 5, -3]
by_absolute = Enum.sort_by(mixed, &abs/1)
IO.inspect(by_absolute) # 输出: [-1, -2, 3, -3, 4, 5]
# 按字符串长度排序
words = ["elixir", "is", "awesome", "programming", "language"]
by_length = Enum.sort_by(words, &String.length/1)
IO.inspect(by_length) # 输出: ["is", "elixir", "awesome", "language", "programming"]
uniq / uniq_by —— 去重
# 去除相邻重复元素
with_dups = [1, 1, 2, 2, 2, 3, 1, 1]
deduped = Enum.uniq(with_dups)
IO.inspect(deduped) # 输出: [1, 2, 3, 1]
# 根据键去重(保留第一个出现的)
users = [
%{id: 1, name: "Alice"},
%{id: 2, name: "Bob"},
%{id: 1, name: "Alice Updated"},
%{id: 3, name: "Charlie"}
]
unique_users = Enum.uniq_by(users, & &1.id)
IO.inspect(unique_users)
# 输出: [
# %{id: 1, name: "Alice"},
# %{id: 2, name: "Bob"},
# %{id: 3, name: "Charlie"}
# ]
管道式操作
Elixir 的管道操作符(|>)允许你将一个函数的输出作为下一个函数的输入,使代码像流水线一样清晰。Enum 模块的函数特别适合管道式调用。
# 计算 1 到 100 之间所有偶数的平方和
result =
1..100
|> Enum.filter(fn x -> rem(x, 2) == 0 end) # 筛选偶数
|> Enum.map(&(&1 * &1)) # 计算平方
|> Enum.sum() # 求和
IO.puts(result) # 输出: 171700
# 更复杂的例子:处理用户数据
users = [
%{name: "Alice", age: 25, city: "Beijing"},
%{name: "Bob", age: 30, city: "Shanghai"},
%{name: "Charlie", age: 35, city: "Beijing"},
%{name: "Diana", age: 28, city: "Guangzhou"},
%{name: "Eve", age: 32, city: "Shanghai"}
]
summary =
users
|> Enum.filter(&(&1.age >= 30)) # 筛选 30 岁以上的
|> Enum.group_by(&(&1.city)) # 按城市分组
|> Map.new(fn {city, users} -> # 计算每组人数
{city, length(users)}
end)
|> Enum.sort_by(&elem(&1, 1), :desc) # 按人数降序排序
IO.inspect(summary)
# 输出: [{"Beijing", 1}, {"Shanghai", 2}](排序后)
# 实际结果: %{"Beijing" => 1, "Shanghai" => 2},按值排序后
惰性枚举:Stream 模块
对于大型集合或无限序列,直接使用 Enum 函数会立即执行所有操作,可能导致性能问题或内存溢出。Stream 模块提供了惰性枚举操作,只有在需要结果时才真正执行计算。
# Enum:立即执行,生成完整列表
result = Enum.map(1..1000000, &(&1 * 2)) # 会立即创建包含 100 万个元素的列表
# Stream:创建惰性管道,不立即执行
lazy_stream =
1..1000000
|> Stream.map(&(&1 * 2)) # 只是描述操作
|> Stream.filter(&(&1 > 1000)) # 同样是描述
|> Stream.take(10) # 只取前 10 个
result = Enum.to_list(lazy_stream) # 只有在这里才真正执行,只产生 10 个元素
何时使用 Stream:
- 处理超大或无限集合
- 构建复杂的转换流水线
- 避免中间列表带来的内存开销
- 需要尽早终止的搜索操作
# 查找前 5 个质数的平方根(无限序列示例)
is_prime = fn n ->
if n < 2 do
false
else
Enum.all?(2..trunc(:math.sqrt(n)), fn d -> rem(n, d) != 0 end)
end
end
primes_sqrt =
Stream.iterate(2, &(&1 + 1)) # 从 2 开始的无限序列
|> Stream.filter(is_prime) # 筛选质数
|> Stream.map(&:math.sqrt/1) # 计算平方根
|> Enum.take(5) # 取前 5 个
IO.inspect(primes_sqrt)
# 输出: [1.0, 1.4142135623730951, 1.7320508075688772, 2.0, 2.23606797749979]
性能考量与最佳实践
理解 Enum 模块的性能特点对于编写高效的 Elixir 代码至关重要。
列表 vs 映射的操作复杂度
# 列表的查找操作是 O(n)
list = [1, 2, 3, 4, 5]
_ = Enum.find(list, &(&1 == 5)) # 必须遍历前 4 个元素
# 映射的查找操作是 O(1)
map = %{a: 1, b: 2, c: 3, d: 4, e: 5}
_ = Map.get(map, :e) # 直接定位
避免不必要的中间列表
numbers = 1..10000
# 低效:每次 Enum.map/filter 都创建新列表
result1 =
numbers
|> Enum.map(&(&1 * 2))
|> Enum.filter(&(&1 > 100))
|> Enum.take(100)
# 高效:使用 Stream 避免中间列表
result2 =
numbers
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 100))
|> Enum.take(100)
常用函数的复杂度参考
| 函数 | 列表复杂度 | 映射复杂度 | 说明 |
|---|---|---|---|
Enum.at |
O(n) | O(n) | 按索引访问 |
Enum.member? |
O(n) | O(n) | 检查成员 |
Enum.map |
O(n) | O(n) | 映射转换 |
Enum.filter |
O(n) | O(n) | 过滤筛选 |
Enum.reduce |
O(n) | O(n) | 聚合归约 |
Enum.sum |
O(n) | O(n) | 求和计算 |
Map.fetch |
- | O(1) | 映射键查找 |
Map.get |
- | O(1) | 映射键获取 |
完整示例:数据处理流水线
以下是一个综合示例,展示如何组合使用 Enum 模块的多个函数来处理真实业务数据。
# 模拟订单数据
orders = [
%{id: 1, customer: "Alice", items: [%{product: "Book", price: 30}, %{product: "Pen", price: 5}], status: :completed},
%{id: 2, customer: "Bob", items: [%{product: "Laptop", price: 1000}], status: :pending},
%{id: 3, customer: "Alice", items: [%{product: "Coffee", price: 15}, %{product: "Cake", price: 10}], status: :completed},
%{id: 4, customer: "Charlie", items: [%{product: "Phone", price: 800}], status: :shipped},
%{id: 5, customer: "Bob", items: [%{product: "Headphones", price: 100}, %{product: "Mouse", price: 30}], status: :completed},
%{id: 6, customer: "Diana", items: [%{product: "Tablet", price: 500}], status: :cancelled}
]
# 业务需求:统计每个完成订单的客户消费总额,并按金额降序排序
customer_spending =
orders
|> Enum.filter(&(&1.status == :completed)) # 1. 只看已完成订单
|> Enum.flat_map(fn order -> # 2. 展开每个订单的所有商品
Enum.map(order.items, fn item -> {order.customer, item.price} end)
end)
|> Enum.group_by(fn {customer, _} -> customer end, fn {_, price} -> price end) # 3. 按客户分组
|> Map.new(fn {customer, prices} -> # 4. 计算每个客户的总消费
{customer, Enum.sum(prices)}
end)
|> Enum.sort_by(&elem(&1, 1), :desc) # 5. 按金额降序排序
IO.inspect(customer_spending)
# 输出: [{"Bob", 130}, {"Alice", 60}]
总结
Enum 模块是 Elixir 编程的基础工具箱,提供了丰富且一致的集合操作接口。掌握这些核心函数及其组合方式,能够让你写出简洁、优雅且高效的函数式代码。
核心要点回顾:
- 遍历操作:使用
each执行副作用,使用map转换元素 - 过滤筛选:
filter保留满足条件的元素,reject排除满足条件的元素 - 聚合归约:
reduce是最强大的聚合工具,可表达任意归纳逻辑 - 分组拆分:
chunk_by和group_by用于按条件组织数据 - 管道组合:善用管道操作符(
|>)构建清晰的数据处理流水线 - 惰性计算:处理大型数据时考虑使用
Stream模块

暂无评论,快来抢沙发吧!