Erlang 从零 · 第 06 课(展开目录)
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 0630 分钟

把文字安全变成整数

用 trim、匿名函数和 try/catch 处理合法与错误输入。

01 · 先看完整例子

一个函数同时处理正确与错误输入

先运行合法输入,再运行错误输入;两次都由同一个匿名函数给出结果。

Erlang
%% 清掉旧绑定,再定义完整解析函数
f().
ParseInteger =
  fun(Text) ->
    Clean = string:trim(Text),
    try binary_to_integer(Clean) of
      Number -> {ok, Number}
    catch
      error:badarg -> {error, not_integer}
    end
  end.

%% 合法 binary 会得到整数
Good = ParseInteger(<<" 42 ">>).

%% 错误 binary 会得到可处理的错误 tuple
Bad = ParseInteger(<<"river">>).

{Good, Bad}.
先对照结果
  • Good 是 {ok, 42}。
  • Bad 是 {error, not_integer}。
  • error:badarg 被 catch 接住,因此第二次调用也正常返回。
02 · 回头拆代码

从第一行往下读

  1. ParseInteger = fun(Text) -> ... end 创建完整匿名函数;调用时直接写 ParseInteger(Binary)。
  2. 文档用 string:trim/1 说明函数名和参数个数;string:trim(Text) 才是真正调用。它先去掉两端空白。
  3. binary_to_integer(Clean) 尝试转换;正常时 of 中的 Number 接住整数,并返回 {ok, Number}。
  4. 错误 binary 会让转换函数抛出 error:badarg;catch 精确匹配它,再返回 {error, not_integer}。
  5. 成功和失败都返回 tuple,后面的代码可以继续模式匹配。
03 · 认清新符号

符号不是暗号

string:trim/1

string 模块、trim 函数、1 个参数。这是文档里的写法,不能直接运行。

string:trim(Text)

把变量 Text 交给函数。这是在调用。

binary_to_integer/1

把只含整数的 binary 转成整数;格式不对时抛出 error:badarg。

fun(Text) -> ... end

创建一个接收 Text 的完整匿名函数。

try ... of ... catch ... end

尝试转换;正常结果走 of,指定异常走 catch。

04 · 代码里的概念

把名字和意思对上

01

arity

函数接收的参数个数。string:trim/1 和 binary_to_integer/1 都接收 1 个参数。

02

匿名函数

用 fun ... end 创建的函数。这里把完整解析流程交给变量 ParseInteger。

03

error:badarg

参数形状不合要求时发出的异常。非数字 binary 交给 binary_to_integer/1 会触发它。

04

try/catch

先尝试一段代码,再按异常类别和原因返回可处理的结果。

05 · 再说清楚

这些写法为什么有用

函数接收输入,返回输出。string:trim(<<" 42 ">>) 先去掉 binary 两端空白,得到 <<"42">>。

文档里的 string:trim/1 表示:string 模块、trim 函数、1 个参数。/1 叫 arity,不是除法,也不是一条能直接粘进 shell 的调用。

binary_to_integer/1 把数字 binary 变成整数;遇到 <<"river">> 这类输入时会抛出 error:badarg。

完整匿名函数用 fun ... end 包住流程。try ... of ... catch ... end 接住预期中的 error:badarg,返回清楚的错误 tuple。

设计选择

为什么这样写

为什么函数名后还要写 /1?

这次选择

BEAM 用“模块、函数名、参数个数”共同确定入口。trim/1 就是接收一个参数的 trim。

代价与边界

同名不同参数个数是不同函数;默认参数只是编译时生成多个 arity,不能把它想成运行时随意少传。

换一把尺子

从 Java、Python、JavaScript 看过来

Java

熟悉的起点
方法可按参数类型和数量重载。
BEAM 在意什么
BEAM 先按名称与 arity 找函数,再由子句按数据形状匹配。
别带错直觉
run/1 与 run/2 是两个明确入口。

Python

熟悉的起点
一个函数常靠默认参数、*args 接收不同数量。
BEAM 在意什么
arity 是函数身份的一部分,并写进导出列表和文档。
别带错直觉
少传一个参数不会自动得到另一个 arity。

JavaScript

熟悉的起点
函数通常容忍少传或多传参数。
BEAM 在意什么
BEAM 调用必须找到精确 arity。
别带错直觉
&1 是匿名函数参数占位,不是 /1 的另一种写法。
06 · 自己改一次

合上答案,动手

补完 ParseAge:<<" 12 ">> 返回 {ok, 12},<<"unknown">> 返回 {error, not_integer}。

练习起点
f().
ParseAge =
  fun(Text) ->
    Clean = string:____(Text),
    try ____(Clean) of
      Number -> {ok, Number}
    catch
      error:____ -> {error, not_integer}
    end
  end.

{ParseAge(<<" 12 ">>), ParseAge(<<"unknown">>)}.

目标结果: 得到 {{ok, 12}, {error, not_integer}}。

卡住了,再看提示

三个空依次是 trim、binary_to_integer 和 badarg。

运行过以后,再看一种答案
一种答案
%% 清理文字,再尝试转换
f().
ParseAge =
  fun(Text) ->
    Clean = string:trim(Text),
    try binary_to_integer(Clean) of
      Number -> {ok, Number}
    catch
      error:badarg -> {error, not_integer}
    end
  end.

{ParseAge(<<" 12 ">>), ParseAge(<<"unknown">>)}.
想一想:为什么 `ParseInteger(<<"river">>)` 没有让整段代码停下来?

binary_to_integer/1 抛出的 error:badarg 被 catch 接住,并转换成了 {error, not_integer}。

带走

记住三句

  1. 1

    文档用 模块:函数/参数个数 写清函数;真正调用时把参数写进括号。

  2. 2

    完整匿名函数可以把清理、转换和错误处理收成一个步骤。

  3. 3

    try/catch 只接住预期异常,并把失败变成清楚的返回值。

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