Chapter 02

Elixir 基础语法

不可变数据与函数式思维——从原子、元组到模式匹配与管道运算符,重建你对编程的直觉

不可变数据(Immutability)

在 Elixir 中,所有数据都是不可变的。变量绑定的值一旦创建,就不能被修改——只能创建新值。这听起来像限制,实则是并发安全的基石。

# 变量绑定(不是赋值!)
x = 5
y = x + 1     # 创建新值 6,x 仍然是 5

# 列表"修改"返回新列表
list = [1, 2, 3]
new_list = [0 | list]   # 前置元素,返回 [0, 1, 2, 3]
# list 仍然是 [1, 2, 3]

# 重绑定(允许,但不是原地修改)
x = 10   # x 现在绑定到新值 10,原来的 5 等待 GC
💡

不可变性 vs 并发:因为数据不可变,多个进程可以安全地持有对同一数据结构的引用——它永远不会被其他进程修改。这消除了共享可变状态带来的竞态条件,无需锁。

基本数据类型

原子(Atom)

原子是以冒号 : 开头的常量,其值就是其名字本身。原子在内存中只存储一次,比较时是 O(1) 操作,大量用于标记、状态标志、键名。

:ok            # 成功标志
:error         # 错误标志
:pending       # 自定义状态
true           # true 本质是原子 :true
false          # false 本质是原子 :false
nil            # nil 本质是原子 :nil

# 常见使用模式:ok/error 元组
{:ok, result} = File.read("data.txt")
{:error, reason} = File.read("nonexistent.txt")

元组(Tuple)

元组用 {} 表示,是固定大小的有序集合,元素存储在连续内存中,按索引访问为 O(1)。常用于返回多值。

point = {10, 20}
{x, y} = point          # 模式匹配解构

response = {:ok, 200, "OK"}
{status, code, message} = response

# elem/2 按索引访问
elem(response, 0)       # :ok
elem(response, 1)       # 200

# tuple_size/1
tuple_size(response)    # 3

列表(List)

列表用 [] 表示,是链表结构(不是数组)。头部插入 O(1),按索引访问 O(n)。适合函数式编程的递归遍历模式。

nums = [1, 2, 3, 4, 5]

# 头尾操作
[head | tail] = nums
# head = 1, tail = [2, 3, 4, 5]

# 列表拼接
[1, 2] ++ [3, 4]      # [1, 2, 3, 4]

# 列表差集
[1, 2, 3] -- [2]     # [1, 3]

# 常用函数
List.first(nums)        # 1
List.last(nums)         # 5
length(nums)           # 5
Enum.at(nums, 2)       # 3(按索引)

映射(Map)

映射用 %{} 表示,键值对结构,键可以是任意类型。当键为原子时有便捷的点访问语法。

# 原子键映射(最常用)
user = %{name: "Alice", age: 30, admin: false}

# 访问
user.name           # "Alice"(点语法,原子键专用)
user[:age]          # 30
Map.get(user, :name) # "Alice"
Map.get(user, :phone, "N/A") # 带默认值

# 更新(返回新映射,原映射不变)
updated = %{user | age: 31}

# 添加/修改键
Map.put(user, :email, "alice@example.com")

# 字符串键映射
config = %{"host" => "localhost", "port" => 4000}
config["host"]   # "localhost"

模式匹配:= 运算符

Elixir 中 = 不是"赋值",而是模式匹配运算符。它尝试让左侧的模式与右侧的值匹配,并绑定其中的变量。

# 简单绑定
x = 42

# 元组解构
{:ok, value} = {:ok, 123}
# value = 123

# 如果不匹配,抛出 MatchError
{:ok, _} = {:error, "not found"}
# ** (MatchError) no match of right hand side value

# _ 是通配符,忽略该位置
{_, second, _} = {1, 2, 3}
# second = 2

# 列表模式匹配
[first | rest] = [10, 20, 30]
# first = 10, rest = [20, 30]

# 嵌套匹配
%{user: %{name: name}} = %{user: %{name: "Bob", age: 25}}
# name = "Bob"

# Pin 运算符 ^ 固定变量值,不重绑定
x = 5
^x = 5   # OK,匹配成功
^x = 6   # ** (MatchError) 6 != 5

函数定义:def 与 defp

defmodule MathUtils do

  # def — 公开函数
  def add(a, b) do
    a + b
  end

  # 单行简写
  def square(x), do: x * x

  # defp — 私有函数,模块外不可调用
  defp validate(x) when x > 0, do: :valid
  defp validate(_), do: :invalid

  # 多子句函数(模式匹配分派)
  def describe(0), do: "零"
  def describe(n) when n > 0, do: "正数"
  def describe(_), do: "负数"

end

管道运算符 |>

管道运算符 |> 将左侧表达式的结果作为第一个参数传给右侧函数,使数据处理链可以从上到下线性阅读,消除嵌套调用的"洋葱"代码。

# 没有管道:嵌套很难读
result = Enum.sum(Enum.filter(Enum.map([1,2,3,4,5], &(&1 * 2)), &(&1 > 4)))

# 使用管道:清晰表达数据流
result =
  [1, 2, 3, 4, 5]
  |> Enum.map(&(&1 * 2))    # [2, 4, 6, 8, 10]
  |> Enum.filter(&(&1 > 4)) # [6, 8, 10]
  |> Enum.sum()              # 24

# 实际应用:处理用户输入
"  hello world  "
|> String.trim()
|> String.split()
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
# "Hello World"

字符串插值

name = "世界"
greeting = "你好,#{name}!"
# "你好,世界!"

# 表达式插值
"1 + 1 = #{1 + 1}"        # "1 + 1 = 2"
"大写:#{String.upcase("elixir")}"  # "大写:ELIXIR"

# 多行字符串(heredoc)
text = """
  第一行
  第二行
  """

Guard 守卫子句

Guard(when 子句)允许在函数子句或模式匹配中添加额外条件,细化匹配逻辑。Guard 只能使用有限的纯函数(确保无副作用)。

defmodule Classifier do

  def classify(n) when is_integer(n) and n > 0, do: "正整数"
  def classify(n) when is_integer(n) and n < 0, do: "负整数"
  def classify(0),                              do: "零"
  def classify(f) when is_float(f),            do: "浮点数"
  def classify(s) when is_binary(s),           do: "字符串"
  def classify(_),                              do: "其他类型"

  # Guard 中可用函数:is_integer/is_float/is_binary/is_list
  # is_atom/is_map/is_nil/is_boolean/is_function
  # abs/rem/div/length/map_size/byte_size/bit_size
  # 比较运算符:>//==/!=/>=/=<
  # 逻辑运算符:and/or/not(Guard 中不能用 &&/||)
end

实战:数据转换管道

综合运用本章知识,构建一个处理用户数据的转换管道:

defmodule DataPipeline do

  @doc "处理原始用户数据列表,返回活跃管理员的邮箱列表"
  def active_admin_emails(users) do
    users
    |> Enum.filter(&active?/1)
    |> Enum.filter(&admin?/1)
    |> Enum.map(&extract_email/1)
    |> Enum.reject(&is_nil/1)
    |> Enum.sort()
  end

  defp active?(%{status: :active}), do: true
  defp active?(_), do: false

  defp admin?(%{role: :admin}), do: true
  defp admin?(_), do: false

  defp extract_email(%{email: email}), do: email
  defp extract_email(_), do: nil
end

# 测试数据
users = [
  %{name: "Alice", role: :admin,  status: :active,   email: "alice@ex.com"},
  %{name: "Bob",   role: :user,   status: :active,   email: "bob@ex.com"},
  %{name: "Carol", role: :admin,  status: :inactive, email: "carol@ex.com"},
  %{name: "Dave",  role: :admin,  status: :active,   email: "dave@ex.com"}
]

DataPipeline.active_admin_emails(users)
# ["alice@ex.com", "dave@ex.com"]

本章小结:Elixir 的基础语法处处体现函数式思想:不可变数据保证并发安全;模式匹配 = 让数据解构自然流畅;管道运算符 |> 使数据流向一目了然;多子句函数 + Guard 替代繁冗的 if/else 分支。下一章深入函数式编程核心工具。