Chapter 04

模式匹配与宏

深度解构、流程控制精粹,以及 Elixir 元编程的魔法——编译时代码生成

深度模式匹配:嵌套结构解构

模式匹配不仅限于顶层结构,可以任意深度嵌套,直接提取你需要的数据:

# 深度嵌套解构
%{
  user: %{
    name: name,
    address: %{city: city}
  }
} = %{
  user: %{
    name: "Alice",
    address: %{city: "北京", zip: "100000"}
  }
}
# name = "Alice", city = "北京"

# 列表中解构结构体
[%{id: first_id} | _] = users
# 提取第一个用户的 id

# 函数参数中的模式匹配
defmodule Handler do
  def process({:ok, %{status: 200, body: body}}) do
    {:ok, body}
  end

  def process({:ok, %{status: status}}) when status >= 400 do
    {:error, "HTTP #{status}"}
  end

  def process({:error, reason}), do: {:error, reason}
end

case / cond / with

case — 多模式分支

case File.read("config.json") do
  {:ok, content} ->
    Jason.decode!(content)
  {:error, :enoent} ->
    IO.puts("配置文件不存在,使用默认配置")
    %{}
  {:error, reason} ->
    raise "读取配置失败: #{inspect(reason)}"
end

cond — 多条件分支(类似 if/else if)

score = 85
grade = cond do
  score >= 90 -> "A"
  score >= 80 -> "B"
  score >= 70 -> "C"
  score >= 60 -> "D"
  true        -> "F"   # 默认分支
end

with — 顺序操作,优雅处理错误链

with 是 Elixir 中最优雅的错误处理结构。它将多个可能失败的操作串联,任意一步失败则跳到 else 分支,避免深层嵌套 case。

def register_user(params) do
  with {:ok, email}    <- validate_email(params["email"]),
       {:ok, password} <- hash_password(params["password"]),
       {:ok, user}     <- create_user(email, password),
       {:ok, _email}   <- send_welcome_email(user) do
    {:ok, user}
  else
    {:error, :invalid_email}    -> {:error, "邮箱格式不正确"}
    {:error, :weak_password}    -> {:error, "密码强度不足"}
    {:error, :email_taken}      -> {:error, "邮箱已被注册"}
    {:error, reason}            -> {:error, "注册失败: #{inspect(reason)}"}
  end
end

# 如果不加 else,失败时 with 直接返回失败值(更简洁)
def process(data) do
  with {:ok, parsed} <- parse(data),
       {:ok, result} <- transform(parsed) do
    {:ok, result}
  end
end

defstruct:结构体

结构体是带有命名字段和默认值的映射,提供编译时字段检查:

defmodule User do
  defstruct [
    :id,
    :email,
    name: "匿名用户",
    role: :user,
    active: true
  ]
end

# 创建结构体
alice = %User{id: 1, email: "alice@ex.com", name: "Alice"}

# 访问字段(与 Map 相同)
alice.name      # "Alice"
alice.role      # :user(默认值)

# 更新(返回新结构体)
%{alice | role: :admin}

# 结构体模式匹配(要求是 User 类型)
def greet(%User{name: name}) do
  "Hello, #{name}!"
end

# 访问不存在的字段会报编译错误(安全)
# alice.phone  # ** (KeyError)

Protocol:多态接口

Protocol 类似面向对象的接口(interface),允许为不同类型实现相同的行为:

# 定义协议
defprotocol Serializable do
  @doc "将数据序列化为字符串"
  def serialize(data)
end

# 为 User 结构体实现
defimpl Serializable, for: User do
  def serialize(%User{id: id, name: name, email: email}) do
    "user:#{id}:#{name}:#{email}"
  end
end

# 为 Map 实现
defimpl Serializable, for: Map do
  def serialize(map) do
    Jason.encode!(map)
  end
end

# 使用(多态分派)
Serializable.serialize(%User{id: 1, name: "Alice"})
Serializable.serialize(%{key: "value"})

宏系统:defmacro(元编程)

Elixir 的宏在编译时运行,可以接收 AST(抽象语法树)并返回新的 AST,实现代码生成。这是 Elixir 比 Erlang 强大得多的地方——Phoenix、Ecto 等框架大量使用宏。

defmodule MyMacros do

  # 宏接收的参数是 Elixir AST(quoted expression)
  defmacro unless(condition, do: block) do
    quote do
      if !unquote(condition) do
        unquote(block)
      end
    end
  end

  # 生成日志函数的宏
  defmacro logged(name, do: block) do
    quote do
      def unquote(name)() do
        IO.puts("开始执行: #{unquote(Atom.to_string(name))}")
        result = unquote(block)
        IO.puts("执行完毕")
        result
      end
    end
  end
end

defmodule MyModule do
  require MyMacros
  import MyMacros

  unless false do
    IO.puts("条件为 false,所以执行")
  end
end

宏的使用原则:优先使用函数,只有函数无法实现时才用宏(如需要操作调用者的 AST)。宏使代码难以调试,过度使用会降低可读性。Elixir 社区有一句话:"Write macros as a last resort"

实战:解析复杂 API 响应

defmodule ApiParser do
  defstruct [:id, :name, :repos]

  @doc "解析 GitHub API 用户响应"
  def parse_github_user(response) do
    with {:ok, body}    <- extract_body(response),
         {:ok, data}    <- Jason.decode(body),
         {:ok, user}    <- build_user(data) do
      {:ok, user}
    end
  end

  defp extract_body(%{status: 200, body: body}), do: {:ok, body}
  defp extract_body(%{status: 404}),             do: {:error, :not_found}
  defp extract_body(%{status: status}),           do: {:error, "HTTP #{status}"}

  defp build_user(%{"id" => id, "login" => name, "public_repos" => repos}) do
    {:ok, %ApiParser{id: id, name: name, repos: repos}}
  end
  defp build_user(_), do: {:error, :invalid_format}
end

本章小结case 用于单值多模式分支;cond 替代 if/else if 链;with 是顺序操作错误链的最佳实践。defstruct 提供类型安全的数据结构;defprotocol 实现多态;defmacro 是 Elixir 元编程的核心,让框架代码可以创建 DSL。下一章进入 Elixir 最强大的领域——OTP 并发。