Erlang 从零 · 第 08 课(展开目录)
01 · 先打开 erl02 · 先认常见 term03 · 分清两种文字04 · 变量只接一次05 · 拆开一列,再筛一次06 · 把文字安全变成整数07 · 用 case 选一条路08 · 让函数继续走09 · 把代码放进模块10 · 条件必须说真话11 · 一层一层取数据12 · 文字先说清编码13 · 让函数子句排好队14 · 带着累加器往前走15 · 先看值,再选路16 · 给模块立规矩17 · 让 rebar3 管项目18 · 让结果有固定形状19 · 把磁盘当成会失败的邻居20 · 把函数交给 list21 · record 留在院里22 · 先写模块之间的约定23 · 把 term 形状写出来24 · 小测试与整段路25 · 把应用磨成命令26 · 先把小项目圈住27 · 把真实文件洗干净28 · 公开入口越少越稳29 · 把约定留在源码旁30 · 打包,也留下线索31 · 按最初任务交卷
ERLANG · 试跑 · LESSON 0835 分钟

让函数继续走

用空 list 停下,用头尾模式处理下一项。

01 · 先看完整例子

复制整段到 erl shell

先看 [] 子句,再看 [Head | Tail] 子句。不要一口追完整条调用链。

Erlang
%% 清掉前几课留在 shell 里的变量
f().

%% Double 保存函数;Loop 只供函数内部递归
Double =
  fun Loop([]) ->
        [];
      Loop([Head | Tail]) ->
        [Head * 2 | Loop(Tail)]
  end.

%% 用外面的名字调用函数
Double([1, 2, 3]).
先对照结果
  • Double([1, 2, 3]) 得到 [2, 4, 6]。
  • 每次调用时,list 都缩短一个元素。
  • 到 Loop([]) 时返回 [],递归停止。
02 · 回头拆代码

从第一行往下读

  1. 输入 [1, 2, 3] 时,Head 是 1,Tail 是 [2, 3]。
  2. 当前一层准备 1 * 2,剩余工作变成 Loop([2, 3])。
  3. Loop 只在这段 fun 内部可见;shell 从外面用变量 Double 调用整个函数。
  4. 分号表示后面还有一个 fun 子句;最后一个子句后直接接 end.。
  5. 最后匹配空 list 子句并返回 [];各层结果再依次接回去。
03 · 认清新符号

符号不是暗号

fun Loop([]) ->

创建带内部名字 Loop 的匿名函数;第一个子句接住空 list。

Loop([Head | Tail]) ->

第二个子句拆出当前元素与剩余元素。

[Head * 2 | Loop(Tail)]

放入当前结果,再让 Loop 处理更短的 Tail。

04 · 代码里的概念

把名字和意思对上

01

递归

函数在自己的定义中再次调用自己。

02

函数子句

同一个函数的不同入口。每个入口接住一种参数形状。

03

停止条件

不再调用自己的情况。这个例子走到空 list 就返回。

05 · 再说清楚

这些写法为什么有用

Erlang 常用递归处理 list:函数处理第一个元素,再把剩余 list 交给自己。

递归必须有停止条件。对于 list,空 list [] 是自然终点。

一个函数可以有几个子句。Erlang 从上往下找形状合适的子句。这里第一个接住空 list,第二个接住 [Head | Tail]。

06 · 自己改一次

合上答案,动手

补出 AddOne 函数,让 list 中每个数字加 1。

练习起点
%% 清掉旧变量,便于反复练习
f().

%% 停止条件保持不变
AddOne =
  fun Loop([]) ->
        [];
      Loop([Head | Tail]) ->
        [____ | Loop(Tail)]
  end.

%% 调用后检查结果
AddOne([2, 3, 4]).

目标结果: AddOne([2, 3, 4]) 得到 [3, 4, 5]。

卡住了,再看提示

当前元素的新值是 Head + 1。

运行过以后,再看一种答案
一种答案
%% f() 只属于 shell,不要写进 .erl 文件
f().

%% AddOne 是外部调用名,Loop 是内部递归名
AddOne =
  fun Loop([]) ->
        [];
      Loop([Head | Tail]) ->
        [Head + 1 | Loop(Tail)]
  end.

%% 应得到 [3,4,5]
AddOne([2, 3, 4]).
想一想:这段递归在哪里停下?

输入变成空 list 时,Loop([]) -> [] 直接返回,不再调用自己。

带走

记住三句

  1. 1

    递归把大问题缩成更小的同类问题。

  2. 2

    缺少 [] 子句时,这个例子会走到空 list 后报 function_clause;问题不缩小,递归才可能一直继续。

  3. 3

    先找停止条件,再读递归子句。

本课结束代码也跑过一次,就把这一课收好。