Chapter 03

函数式编程核心

掌握 Enum、Stream、匿名函数与尾递归——Elixir 数据处理的完整工具箱

高阶函数:Enum 模块

Enum 模块提供了一组操作可枚举数据(列表、映射等)的函数,是 Elixir 中使用最频繁的模块。

Enum.map — 变换

# 将每个元素乘以 2
Enum.map([1, 2, 3], fn x -> x * 2 end)
# [2, 4, 6]

# 提取结构体字段
users = [%{name: "Alice"}, %{name: "Bob"}]
Enum.map(users, &(&1.name))
# ["Alice", "Bob"]

Enum.filter — 过滤

Enum.filter([1,2,3,4,5], &(rem(&1, 2) == 0))
# [2, 4](保留偶数)

# reject 是 filter 的反面
Enum.reject([1,2,3,4,5], &(rem(&1, 2) == 0))
# [1, 3, 5](过滤偶数)

Enum.reduce — 归约

# 求和
Enum.reduce([1,2,3,4,5], 0, fn x, acc -> x + acc end)
# 15

# 构建映射(频率统计)
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
Enum.reduce(words, %{}, fn word, acc ->
  Map.update(acc, word, 1, &(&1 + 1))
end)
# %{"apple" => 3, "banana" => 2, "cherry" => 1}

其他常用 Enum 函数

Enum.sort([3,1,2])                          # [1, 2, 3]
Enum.sort_by(users, &(&1.age))               # 按 age 排序
Enum.uniq([1,2,2,3])                         # [1, 2, 3]
Enum.group_by(users, &(&1.role))             # 按 role 分组
Enum.flat_map([[1,2],[3,4]], &(&1))           # [1, 2, 3, 4]
Enum.zip([1,2,3], ["a","b","c"])            # [{1,"a"},{2,"b"},{3,"c"}]
Enum.any?([1,2,3], &(&1 > 2))               # true
Enum.all?([1,2,3], &(&1 > 0))               # true
Enum.count([1,2,3])                          # 3
Enum.min([3,1,2]) / Enum.max([3,1,2])        # 1 / 3
Enum.take([1,2,3,4], 2)                     # [1, 2]
Enum.chunk_every([1,2,3,4], 2)              # [[1,2],[3,4]]

匿名函数

Elixir 有两种写匿名函数的语法:

# 完整 fn 语法
double = fn x -> x * 2 end
double.(5)   # 10(注意调用时需要加点 .)

# 捕获运算符 & 简写(常用于 Enum 回调)
double = &(&1 * 2)    # &1 是第一个参数
add    = &(&1 + &2)   # &1, &2 分别是第一、二个参数

# 捕获已命名函数
Enum.map(["a","b"], &String.upcase/1)
# ["A", "B"]
# &String.upcase/1 捕获 arity 为 1 的 String.upcase 函数

# 多子句匿名函数
classify = fn
  x when x > 0 -> :positive
  x when x < 0 -> :negative
  0            -> :zero
end

闭包

匿名函数会捕获创建时的外部环境,形成闭包:

defmodule Counter do
  def make_adder(n) do
    fn x -> x + n end   # 闭包捕获 n
  end
end

add5  = Counter.make_adder(5)
add10 = Counter.make_adder(10)

add5.(3)    # 8
add10.(3)   # 13

递归与尾递归优化

Elixir 没有循环语句(for 是推导式,不是循环)。重复操作通过递归实现。BEAM 对尾递归(最后一步是递归调用)做了优化,使其不会增长调用栈。

defmodule MyList do

  # 普通递归求长度(非尾递归,会增长调用栈)
  def length_naive([]),         do: 0
  def length_naive([_ | rest]), do: 1 + length_naive(rest)

  # 尾递归版本(使用累加器)
  def length_tail(list), do: length_tail(list, 0)

  defp length_tail([], acc),        do: acc
  defp length_tail([_ | rest], acc), do: length_tail(rest, acc + 1)
  # 最后一步直接跳转到递归调用,无需保存栈帧

  # 尾递归反转列表
  def reverse(list), do: reverse(list, [])

  defp reverse([], acc),          do: acc
  defp reverse([h | t], acc),    do: reverse(t, [h | acc])
end

实际中不必手写递归:Enum 模块已经提供了所有常见操作(map/filter/reduce 等),且内部使用了尾递归优化。只有在实现自定义数据结构或算法时才需要手写递归。

Stream:惰性计算

Stream 模块提供惰性(lazy)版本的 Enum 操作。它不立即计算,而是构建一条处理管道,只在需要时(如 Enum.to_list)才执行。适合处理大数据集或无限序列。

# 处理大文件(逐行,不全部载入内存)
File.stream!("huge_log.txt")
|> Stream.filter(&String.contains?(&1, "ERROR"))
|> Stream.map(&String.trim/1)
|> Enum.take(100)   # 只取前100条错误,找到即停

# 无限序列
Stream.iterate(1, &(&1 * 2))   # 1, 2, 4, 8, 16, ...
|> Enum.take(10)
# [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]

# 生成斐波那契数列
Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
|> Enum.take(10)
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Enum vs Stream 对比
# Enum:立即计算,每步产生中间列表,适合小数据
# Stream:惰性管道,单次遍历完成所有操作,适合大/无限数据

for 推导式(Comprehension)

# 基本推导:生成 1-10 的平方
for x <- 1..10, do: x * x
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# 带过滤器
for x <- 1..10, rem(x, 2) == 0, do: x
# [2, 4, 6, 8, 10]

# 多重生成器(笛卡尔积)
for x <- [1,2], y <- ["a","b"], do: {x, y}
# [{1,"a"},{1,"b"},{2,"a"},{2,"b"}]

# into 收集到映射
for {k, v} <- %{a: 1, b: 2}, into: %{}, do: {k, v * 10}
# %{a: 10, b: 20}

# 模式匹配解构
for %{name: name, age: age} <- users, age >= 18, do: name

defmodule:模块组织

defmodule MyApp.Accounts.User do
  @moduledoc """
  用户账户模块。
  处理用户的创建、认证与权限管理。
  """

  @default_role :user   # 模块属性(编译时常量)

  @doc "创建新用户,返回 {:ok, user} 或 {:error, reason}"
  def create(params) do
    # ...
  end

  @doc false   # 文档化但不显示在 ExDoc 中
  def internal_thing(), do: :ok
end

# 别名简化模块名
alias MyApp.Accounts.User
User.create(params)

# import 引入函数,直接调用
import Enum, only: [map: 2, filter: 2]
map([1,2,3], &(&1 * 2))

实战:CSV 数据处理流水线

defmodule CSVProcessor do
  @doc "从 CSV 字符串解析用户数据,返回有效用户列表"
  def parse_users(csv_string) do
    csv_string
    |> String.split("\n", trim: true)
    |> Enum.drop(1)                    # 跳过标题行
    |> Enum.map(&parse_row/1)
    |> Enum.filter(&valid?/1)
    |> Enum.sort_by(&(&1.name))
  end

  defp parse_row(row) do
    case String.split(row, ",") do
      [name, age_str, email] ->
        %{
          name:  String.trim(name),
          age:   parse_age(age_str),
          email: String.trim(email)
        }
      _ -> nil
    end
  end

  defp parse_age(str) do
    case Integer.parse(String.trim(str)) do
      {n, _} -> n
      :error -> nil
    end
  end

  defp valid?(nil), do: false
  defp valid?(%{name: ""}), do: false
  defp valid?(%{age: nil}), do: false
  defp valid?(%{age: age}) when age < 0 or age > 150, do: false
  defp valid?(_), do: true
end

# 测试
csv = """
name,age,email
Alice,30,alice@ex.com
Bob,,bob@ex.com
Carol,25,carol@ex.com
"""

CSVProcessor.parse_users(csv)
# [%{name: "Alice", age: 30, email: "alice@ex.com"},
#  %{name: "Carol", age: 25, email: "carol@ex.com"}]

本章小结:Elixir 的函数式工具极为丰富:Enum 处理有限数据集;Stream 用于大文件和无限序列;匿名函数与 & 捕获运算符配合 Enum 极为简洁;尾递归优化保证大数据递归的安全;for 推导式优雅地处理多维遍历。下一章深入模式匹配高级用法与宏系统。