文章目录

Elixir 函数式编程:Enum 模块

发布于 2026-04-05 13:24:16 · 浏览 11 次 · 评论 0 条

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)。这相当于其他语言中的 forEachfor...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 —— 反向筛选

rejectfilter 的对立面,返回所有不满足条件的元素。

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 —— 核心聚合函数

reduceEnum 模块中最强大、最灵活的函数。它接收一个可枚举对象、一个初始累加器和一个回调函数,逐个元素处理并累积结果。

# 基本用法:求和
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

  1. 处理超大或无限集合
  2. 构建复杂的转换流水线
  3. 避免中间列表带来的内存开销
  4. 需要尽早终止的搜索操作
# 查找前 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 编程的基础工具箱,提供了丰富且一致的集合操作接口。掌握这些核心函数及其组合方式,能够让你写出简洁、优雅且高效的函数式代码。

核心要点回顾

  1. 遍历操作:使用 each 执行副作用,使用 map 转换元素
  2. 过滤筛选filter 保留满足条件的元素,reject 排除满足条件的元素
  3. 聚合归约reduce 是最强大的聚合工具,可表达任意归纳逻辑
  4. 分组拆分chunk_bygroup_by 用于按条件组织数据
  5. 管道组合:善用管道操作符(|>)构建清晰的数据处理流水线
  6. 惰性计算:处理大型数据时考虑使用 Stream 模块

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文