第 R1 站 · 可选复习展开探险地图
00装好工具前置准备01起跑线前置准备02进程与信箱R1Elixir 数据流水线可选复习R2读懂 Erlang可选复习03消息与超时04OTP 消息章法05会重启的监督树06给并发设上限07BEAM 节点失联X1两种语言对暗号双语扩展X2双语搭档双语扩展08可靠任务调度器
首页/BEAM 主线/第 R1 站
R1
语言入门探索可选复习Elixir

Elixir 数据流水线

先修剪,再筛选,再分类;每个小函数只做好一件事。

5小关卡
约 6 小时 · 建议分 5 次可以分几次
QUESTION · 本站只追这一问

【语言跳级复习】怎样把会遇到空行、未知级别和坏格式的日志清洗器,拆成可单独测试的流水线?

先看现场

一条 WARN 日志让整条流水线停下

输入里混着空行、空格与 WARN。解析器只认识 INFO 和 ERROR,于是抛出 FunctionClauseError,后面的统计也没有机会运行。

能够观察到
  • 能复现问题的最小输入,其中保留一条 WARN
  • trim、filter 与 parse 每一步的中间结果
  • 失败测试,以及异常指出的函数子句
为什么学这一站

它要解决什么

长函数容易混入太多工作。先用模式认出数据,再把转换拆成小函数。管道负责把上一步结果交给下一步。

走完这一站

你会做到

  • 能先用模式认出数据,再用 guard(附加条件)和多子句函数选择处理方法

  • 能说清 Enum 是马上完成一批工作,Stream 是等到结果被需要时再逐步处理

  • 能给不读文件、不发消息的纯函数写出一组小而清楚的 ExUnit 测试

出发前
  • 知道 BEAM 里有小进程和 mailbox,并见过一次模式匹配
  • 知道 list 用来排一列数据,map 用键找到对应的值
同一件事,两种写法

先看做什么,再看怎么写

两种写法都在清理、解析并统计同一份数据。

当前代码
Elixir
# 把多行日志整理成各级别出现次数
defmodule LogSummary do
  def summarize(lines) do
    # 先清理空白,再解析每一行
    lines
    |> Stream.map(&String.trim/1)
    |> Stream.reject(&(&1 == ""))
    |> Enum.map(&parse_line/1)
    |> Enum.frequencies_by(& &1.level)
  end

  # 用字符串前缀直接取出级别和正文
  defp parse_line("ERROR " <> message),
    do: %{level: :error, message: message}

  defp parse_line("INFO " <> message),
    do: %{level: :info, message: message}
end
LAB
动手

交换两道工序

约 15–25 分钟

交换 trim 和 reject。先猜纯空格行会去哪,再运行同一组数据。

  1. 01

    准备 Elixir 字符串列表 [" INFO boot ", " ", "ERROR timeout"]

  2. 02

    先运行“去掉两端空格,再删除空字符串”的顺序,记下结果

  3. 03

    把 reject 移到 trim 前面,再用完全相同的输入运行一次

复制到终端,按回车
# 运行全部测试,并显示每项测试名称与耗时
mix test --trace
你会看到
  • 先 trim 再 reject 时,只有空格的那一行会被删掉
  • 先 reject 再 trim 时,那一行一开始还不是空字符串,随后可能闯进解析器
故意弄坏

传入 WARN slow,再删掉兜底子句。查看 FunctionClauseError 保留了哪些线索。

这次能看清

数据经过步骤的先后顺序,会改变最后能被模式匹配识别的内容。

这次还不能说明

这不能说明 Stream 比 Enum 快。数据量和消费方式都会影响选择。

先认词

这段代码里的关键词

01

模式匹配

左边是形状模板,右边是待检查的数据。对得上就取出内容,对不上就换另一条处理路径。这里的 = 不只是赋值。

02

多子句函数

同一函数可准备多个入口。程序从上往下寻找第一个模式与 guard 都符合的子句。元数是参数个数。

03

管道

|> 把左边结果交给右边函数,作为第一个参数。它让步骤更清楚,但不会替你拆好函数。

从代码里认出章法

这里用了哪些设计模式

01

Pipes and Filters

让每一步只做一种转换,输出再交给下一步。

02

Single Responsibility

让清理、解析与汇总各自成为可测试的小函数。

想一想

哪种情况更适合先考虑 Stream?

轮到你

整理一份日志

做一个 Mix 工具:读取日志,跳过空行,统计三个级别,再找出最常见的三条消息。

提示 1先迈一步

先只写“看懂一行日志”的纯函数,再把读取文件放在最外面。

提示 2再缩小一点

遇到不认识的行,不要偷偷扔掉,把它作为线索用 {:error, line} 返回。

提示 3离答案很近了

准备三种测试:带空格的内容、未知级别和空文件。

提示 4从终点往回想

先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。

过关条件
  • 整理日志和读取文件是两个分开的步骤

  • 遇到未知行时会留下清楚的错误结果

  • ExUnit 测试照顾到正常情况、边角情况和错误情况

带走

记住三句话

  1. 1

    先看清数据长什么样,再安排它要经过哪些步骤。

  2. 2

    管道负责把结果交给下一步;真正让代码好懂的,仍是小而清楚的函数。

  3. 3

    Stream 会延后工作,但不是“更快版 Enum”,要根据数据和目标来选。

再读一点

去看原版资料

Elixir 基础类型EnumExUnit
本站结束实验做过,答案也想过,就把这一站收好。