第 R1 站 · 可选复习展开探险地图
Elixir 数据流水线
先修剪,再筛选,再分类;每个小函数只做好一件事。
【语言跳级复习】怎样把会遇到空行、未知级别和坏格式的日志清洗器,拆成可单独测试的流水线?
一条 WARN 日志让整条流水线停下
输入里混着空行、空格与 WARN。解析器只认识 INFO 和 ERROR,于是抛出 FunctionClauseError,后面的统计也没有机会运行。
- 能复现问题的最小输入,其中保留一条 WARN
- trim、filter 与 parse 每一步的中间结果
- 失败测试,以及异常指出的函数子句
它要解决什么
长函数容易混入太多工作。先用模式认出数据,再把转换拆成小函数。管道负责把上一步结果交给下一步。
你会做到
能先用模式认出数据,再用 guard(附加条件)和多子句函数选择处理方法
能说清 Enum 是马上完成一批工作,Stream 是等到结果被需要时再逐步处理
能给不读文件、不发消息的纯函数写出一组小而清楚的 ExUnit 测试
- 知道 BEAM 里有小进程和 mailbox,并见过一次模式匹配
- 知道 list 用来排一列数据,map 用键找到对应的值
先看做什么,再看怎么写
两种写法都在清理、解析并统计同一份数据。
# 把多行日志整理成各级别出现次数
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交换两道工序
交换 trim 和 reject。先猜纯空格行会去哪,再运行同一组数据。
- 01
准备 Elixir 字符串列表
[" INFO boot ", " ", "ERROR timeout"] - 02
先运行“去掉两端空格,再删除空字符串”的顺序,记下结果
- 03
把
reject移到trim前面,再用完全相同的输入运行一次
# 运行全部测试,并显示每项测试名称与耗时
mix test --trace- 先
trim再reject时,只有空格的那一行会被删掉 - 先
reject再trim时,那一行一开始还不是空字符串,随后可能闯进解析器
传入 WARN slow,再删掉兜底子句。查看 FunctionClauseError 保留了哪些线索。
数据经过步骤的先后顺序,会改变最后能被模式匹配识别的内容。
这不能说明 Stream 比 Enum 快。数据量和消费方式都会影响选择。
这段代码里的关键词
模式匹配
左边是形状模板,右边是待检查的数据。对得上就取出内容,对不上就换另一条处理路径。这里的 = 不只是赋值。
多子句函数
同一函数可准备多个入口。程序从上往下寻找第一个模式与 guard 都符合的子句。元数是参数个数。
管道
|> 把左边结果交给右边函数,作为第一个参数。它让步骤更清楚,但不会替你拆好函数。
这里用了哪些设计模式
Pipes and Filters
让每一步只做一种转换,输出再交给下一步。
Single Responsibility
让清理、解析与汇总各自成为可测试的小函数。
哪种情况更适合先考虑 Stream?
整理一份日志
做一个 Mix 工具:读取日志,跳过空行,统计三个级别,再找出最常见的三条消息。
提示 1先迈一步
先只写“看懂一行日志”的纯函数,再把读取文件放在最外面。
提示 2再缩小一点
遇到不认识的行,不要偷偷扔掉,把它作为线索用 {:error, line} 返回。
提示 3离答案很近了
准备三种测试:带空格的内容、未知级别和空文件。
提示 4从终点往回想
先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。
整理日志和读取文件是两个分开的步骤
遇到未知行时会留下清楚的错误结果
ExUnit 测试照顾到正常情况、边角情况和错误情况
记住三句话
- 1
先看清数据长什么样,再安排它要经过哪些步骤。
- 2
管道负责把结果交给下一步;真正让代码好懂的,仍是小而清楚的函数。
- 3
Stream 会延后工作,但不是“更快版 Enum”,要根据数据和目标来选。