Erlang的类型介绍

前言:

Erlang 有一种类型表示法,它可以用来定义新的数据类型并给代码添加类型注解( typeannotation)。类型注解能让代码更易理解和维护,还可以在编译时检测错误。
在这我们将介绍类型表示法,并讨论两个可以用来寻找代码错误的程序。将要讨论的这两个程序名为dialyzer typer ,它们存在于标准 Erlang 分发套装里。 dialyzer 代表“DIscrepancy AnaLYZer for ERlang programs ”(用于 Erlang 程序的差错分析器),它的作用名副其实:寻找Erlang 代码里的差错。 typer 提供了程序里使用的类型信息。 dialyzer typer 不需要 类型注解就能完美工作,但如果你给程序添加了类型注解,这些工具的分析质量就会变得更高。

一:指定数据和函数类型

下面这个模块导出了一个名为 plan_route/2 的函数。该函数的输入和返回类型由一个 类型规范
type specification )指定, 类型声明 type declaration )里还定义了三个新的类型。
%% walks.erl
-module(walks)
-export([plan_route/2]).

-spec plan_route(point(), point()) -> route().
-type direction() :: north | south | east | west.
-type point() :: {integer(), integer()}.
-type route() :: [{go,direction(), integer()}].

...
对它们的解读如下:
(1) -spec plan route(point(), point()) -> route().
%% 它的意思是如果调用plan_route/2函数时使用了两个类型为point()的参数,此函数就
%% 会返回一个类型为route()的对象

(2) -type direction() :: north | south | east | west.
%% 引入一个名为direction()的新类型,它的值是下列原子之一:north、south、east或west

(3) -type point() :: {integer(), integer()}.
%% 指point()类型是一个包含两个整数的元组(integer()是预定义类型)

(4) -type route() :: [{go, direction(), integer()}].
%% 将route()类型定义为一个由三元组(3-tuple)构成的列表,每个元组都包含一个原子go,
%% 一个类型为direction的对象和一个整数。[X]这种表示法的意思是一个由X类型构成的列表
想象执行plan_route后会看到如下输出:
1> walks:p1 an route({1,10},{25,57}).
[{go, east, 24},
 {go, north, 47},
...
]
当然,我们完全不知道 plan_route 函数到底会不会返回,也许它直接崩溃了,不会返回任
何值。但如果它真的返回了一个值,那么当输入参数的类型是 point() 时,返回值的类型应该是
route() 。同样不知道之前那个表达式里的数字有什么含义。它们是英里、公里、厘米还是其他?
所知道的就是类型声明能告诉我们的,即它们都是整数。
为了增强类型的表达能力,可以用描述性变量给它们加上注解。例 如:
-spec planroute(From :: point(), To :: point()) -> ...
类型注解里的名称 From (起点) To (终点)让用户对这些参数在函数里扮演的角色有了
一定的了解。它们还用来在文档里的名称和类型注解里的变量之间建立联系。 Erlang 的官方文档
对于编写类型注解有着严格的规定,使类型注解里的名称能够对应文档里的名称。
声明我们的路径( route From 开始并且 From 是一对整数可能足以描述该函数,也可能需
要更多信息,这需要根据上下文确定。可以通过添加更多的信息来轻松改进类型定义。比如这
么写:
-type angle() :: {Degrees :: 0..360, Minutes :: 0..60, Seconds :: 0..60}.
-type position() :: {latitude | longitude, angle()}.
-spec plan_routel(From :: position(), To :: position()) -> ...
这种新形式提供了很多的信息,但是仍然免不了让人猜测。我们可能会猜想角度的单位是度 (degree ),因为允许值的范围是 0 360 ,但它们也可能是弧度,这样的话就猜错了。

二:Erlang 的类型表示法

1.类型的语法

类型定义可以使用以下的非正式语法:
T1 :: A | B | C ...
它的意思是 T1 被定义为 A B C其中之一。用这种表示法,可以定义一些 Erlang 类型如下:
Type :: any() | none() | pid() | port() | reference() | []
        | Atom | binary() | float() | Fun | Integer | [Type]
        | Tuple | Union | UserDefined

Union :: Typel | Type2 ...

Atom :: atom() | Erlang_Atom

Integer :: integer() | Min .. Max

Fun :: fun() | fun((...) -> Type)

Tuple :: tuple() | {T1, T2, ..., Tn}
在上面的例子中, any() 是指任意 Erlang 数据类型 X() 是指一个类型为 XErlang 对象,而
none() 标识则用来指代永不返回的函数类型
[X] 这种表示法指代一个由 X 类型构成的列表, {T1, T2,  ..., Tn} 指代一个大小为 n ,参数
类型分别为 T1, T2, ... Tn 的元组。
定义新的类型可以使用以下语法:
-type NewTypeName(TVar1, TVar2, ..., TVarN) :: Type.
TVar1 TVarN 是可选的类型变量, Type 是一个类型表达式。由此我们还可以使用这种方法创建更多的例子,如:
-type onoff() :: on | off.
-type person() :: {person, name(), age()}.
-type people() :: [person()].
-type name()  :: {firstname, string()}.
-type age() :: integer().
-type dict(Key,Val) :: [{Key,Val}].
根据这些规则, {firstname, "dave"} 属于name()类型 [{person, {firstname, "john"},
35}, {person, {firstname,"mary"}, 26}] 属于people()类型,以此类推。 dict(Key,Val)
类型展示了类型变量的用法,它把一个字典类型定义为由 {Key, Val} 元组构成的列表。

2.预定义类型

除了类型语法以外,还有下面这些预定义的类型别名:
-type term() :: any().

-type boolean() :: true | false.

-type byte() :: 0..255.

-type char() :: 0..16#10ffff.

-type number() :: integer() | float().

-type list() :: [any()].

-type maybe_improper_list() :: maybe_improper_list(any(), any()).

-type maybe_improper_list(T) :: maybe_improper_list(T, any()).

-type string() :: [char()].

-type nonempty_string() :: [char(), ...].

-type iolist() :: maybe_improper_list(byte() | binary() | {iolist(), binary()} | []).

-type module() :: atom().

-type mfa() :: {atom(), atom(), atom()}.

-type node() :: atom().

-type timeout() :: infinity | non_neg_integer(). 

-type no_return() :: none().
maybe_improper_list 用于指定带有非空 non-nil 最终列表尾的列表类型。这样的列表
很少会用到,但是指定它们的类型是可能的!
还有少量其他的预定义类型。 non_neg_integer() 是一个非负的整数 pos_integer()
一个正整数 neg_integer() 是一个负整数。最后, [X, ...] 这种表示法的意思是一个 X类型
构成的非空列表

3.指定函数的输入输出类型

函数规范说明了某个函数的参数属于何种类型,以及该函数的返回值属于何种类型。函数规范的编写方式如下:
-spec functionName(T1, T2, ..., Tn) -> Tret when
    Ti :: Typei,
    Tj :: Typej,
    ...
这里的 T1, T2,..., Tn 描述了某个函数的参数类型, Tret 描述了函数返回值的类型。如果
有必要,可以在可选关键字 when 后面引入额外的类型变量。
可以举个例子:
-spec file:open(FileName,Modes) -> {ok,Handle} | {error,Why} when
    FileName :: string(),
    Modes :: [Mode],
    Mode :: read | write | ...
    Handle :: file_handle(),
    why :: error_term().
说明如果打开 FileName 文件,得到的返回值不是 {ok, Handle} 就是 {error, Why}
FileName 是一个字符串, Modes 是一个由 Mode 组成的列表,而 Mode read write 等模式中的
一个。上面这个函数规范可以用多种等价的方式编写。当然,也可以像下面这样不使用限定词
when
-spec file:open(string(), [read | write | ...] -> {ok,Handle} | {error,Why}
这样做的问题在于:首先,失去了 FileName Modes 这些描述性的变量;其次,类型规范的
长度大大增加,导致阅读和在打印文档里格式化的难度增加。在理想情况下,程序的后面应该附
有文档,而如果没有给函数的参数命名,就无法在文档里引用它们。
使用限定词 when 的函数的任何文档都可以毫无歧义地引用打开的文件,方法是使用其名称 FileName。如果丢弃了限定词 when那么文档在引用打开的文档时就不得不称其为“open函数的第一个参数”,这种迂回的说法对第一种规范编写方式而言是不必要的。
类型变量可以在参数里使用,如下所示:
-spec lists :: map(fun((A) -> B), [A]) -> [B].
-spec lists :: filter(fun((X) -> bool()), [X]) -> [X].
它的意思是 map 函数接受一个从 A 类型到 B 类型的函数和一个由 A类型对象组成的列表,然后返回一个由 B 类型对象组成的列表,以此类推。

4.导出类型和本地类型

有时候我们希望某个类型的定义局限在该定义所属的模块内部,而在另一些情况下则希望把
此类型导出至别的模块。想象一下有两个模块 a b a 模块生成rich_text类型的对象, b 模块则
操作这些对象。在 a 模块里加入以下注解:
-module(a).
-type rich_text() :: [{font(), char()}].
-type font() :: integer().
-export_type([rich_text/0, font/0]).

...
我们不仅声明了一个富文本和一个字体类型,还用注解 -export_type(...) 导出了它们。
假设 b 模块能操作富文本的实例,比如内含一个计算富文本对象长度的 rich_text_length
函数。编写此函数的类型规范如下:
-module(b).
...
-spec rich_text_length(a:rich_text()) -> integer().
...

...
rich_text_length 的输入参数使用了完全限定的类型名 a:rich_text() ,它是指从 a 模块
导出的 rich_text() 类型。

5.不透明类型

在上述中, a b 这两个模块通过操作富文本对象的内部结构相互协作。但是,我们也许
希望隐藏富文本数据结构的内部细节,使得只有创建此数据结构的模块才了解类型的细节。可以
通过一个例子来更好地理解这一点。假设 a 模块的开头部分如下:
-module(a).
-opaque rich_text() :: [{font(), char()}].
-export_type([rich_text/0]).

-export [make_text/1, bounding_box/1]).
-spec make_text(string()) -> rich_text().
-spec bounding_box(rich_text()) -> {Height :: integer(), Width :: integer()}.

...
下面这个语句:
-opaque rich_text() :: [{font(), char()}].
创建了一个名为 rich_text() 的不透明类型( opaque type)。再来看一些尝试操作富文本对象的代码:
-module(b).
...

do this() ->
    X = a:make_text("hello world"),
    {W,H} = a:bounding_box(X).
b 模块永远不需要知道变量 X 的内部结构。 X 是在 a 模块里创建的,调用 bounding_box(X)
会把它传回 a。现在假设我们编写了一段利用了某些rich_text对象外形知识的代码。比如,假设创建了一个富文本对象,然后询问需要什么字体来渲染此对象。我们也许会这么写:
-module(c).
...

fonts_in(Str) ->
    X = a:make_text(Str),
    [F || {F, _} <- X].
在列表推导里,我们“知道” X 是一个由双元组构成的列表,而 a模块里声明过 make_text 的返回类型是不透明的,意思是我们不该知道此类型的任何内部结构信息。利用此类型的内部结构信息被称为抽象违规 abstraction violation ),如果正确声明了相关函数的类型可见性,这一违规就可以被dialyzer 检测出来。

三:dialyzer 教程

第一次运行 dialyzer 时,需要为打算使用的所有标准库类型建立缓存。这个操作只需进行一次。启动dialyzer 后它会告诉你怎么做,如:
$ dialyzer
Checking whether the PLT /Users/joe/.dialyzer_plt is up-to-date...
dialyzer: Could not find the PLT: /Users/joe/.dialyzer_plt
Use the options:
--build plt       to build a new PLT;   or
--add_to_plt   to add to an existing PLT
For example, use a command like the following:
dialyzer --build_plt --apps erts kernel stdlib mnesia
...

PLT是Persistent Lookup Table(持久性查询表)的缩写。PLT应当包含标准系统里所有类型的缓存。生成PLT需要花费几分钟的时间。我们输入的第一个命令会生成ertsstdlibkernel的PLT

$ dialyzer --build_plt --apps erts kernel stdlib
Compiling some key modules to native code...done in 0m59.78s
Creating PLT /Users/joe/.dialyzer_plt  ...
Unknown functions:
compile:file/2
compile:forms/2
compile:noenv_forms/2
compile:output_generated/1
crypto:des3_cbc_decrypt/5
crypto:start/0
Unknown types:
compile:option/0
done in 4m3.86s
done (passed successfully)

 现在PLT已经建成,我们准备好运行dialyzer了。出现未知函数的警告是因为列出的函数存在于外部的应用中,不属于我们选择进行分析的这三个。dialyzer很很保守。如果它抱怨了,那就说明程序里确实存在着不一致性。制作dialyzer项目的一个目标就是消除虚假警告消息,即并非针对真正错误的警告消息。在下面我们会给出一些错误程序的例子,对这些程序运行dialyzer,并展示dialyzer能够报告哪些类型的错误。

1.错误使用内置函数的返回值

%% dialyzer/test1.erl
-module(test1).
-export([f1/0]).

f1()->
    X = erlang:time(),
    seconds(X).

seconds({_Year, Month, _Day, Hour, Min, sec}) ->
    (Hour 60 Min)*60 + Sec.

$ dialyzer testl.erl.
Checking whether the PLT /Users/joe/.dialyzer_plt is up-to-date...yes
Proceeding with analysis...
test1.erl:4:Function f1/0 has no local return
testl.erl:6:The call testl:seconds(X ::
        {non_neg_integer(),non_neg_integer(),non_neg_integer()})
        will never return since it differs in the lst argument
        from the success typing arguments:({_,_,_number(),number(),number()})
testl.erl:8: Function seconds/1 has no local return
test1.erl:8: The pattern {_Year,_Month,_Day,Hour,Min,Sec}can never
        match the type {non_neg_integer(),non_neg_integer(),non_neg_integer()}

done in 0m0.41s

这段相当吓人的错误消息出现的原因是 erlang:time() 返回一个名为 {Hour, Min, Sec}
三元组,而不是我们期望的六元组。 Function f1/0 has no local return (函数 f1/0 没有
本地返回)的意思是 f1/0 会崩溃。 dialyzer 知道 erlang:time() 的返回值是 {non_neg_integer(),
non_neg_integer(), non_neg_integer()} 类型的一个实例,因此绝不可能匹配 seconds/1
数的六元组模式。

2.内置函数的错误参数

可以通过 dialyzer 来了解是否用错误的参数调用了内置函数。这方面的例子如下:
%% dialyzer/test2.erl
-module(test2).
-export([f1/0]).

f1()->
    tuple_size(list_to_tuple({a,b,c})).

$ dialyzer test2.erl
test2.erl:4:Function f1/0 has no local return
test2.erl:5:The call erlang:list_to_tuple({'a','b','c'})
will never return since it differs in the 1st argument from the
success typing arguments:([any()])

 它告诉我们list_to_tuple期望的参数是[any()]类型的,而不是{'a','b','c'}

3.错误的程序逻辑

dialyzer 还可以检测出有问题的程序逻辑。这里有一个例子:
%% dialyzer/test3.erl
-module(test3).
-export([test/0, factorial/1]).

test() -> factorial(-5).

factorial(0) -> 1;
factorial(N) -> N*factorial(N-1).

$ dialyzer test3.erl
test3.erl:4:Function test/0 has no local return
test3.erl:4:The call test3:factorial(-5) will never return since
it differs in the 1st argument from the success typing
arguments:(non_neg_integer())

 这其实是相当厉害的。阶乘(factorial)的定义有问题。如果用负数作为参数调用阶乘,这个程序就会进入无限循环,蚕食栈空间,最终Erlang会因为内存耗尽而崩溃。dialyzer根据阶乘的参数属于non_neg_integer()类型推断出factorial(-5)这个调用是错误的。因为dialyzer没有打印出它推测的函数类型,所以我们可以来问问typer

$ typer test3.erl
-spec test() -> none().
-spec factorial(non_neg_integer()) -> pos_integer().

typer 推测 factorial 的类型是 (non_neg_integer())  ->  pos_integer() ,而 test() 的类

型是none()

总的来说,这些程序的推理过程如下: 递归的基本情形是factorial(0),因此要让factorial的参数
变成0,factorial(N-1)这个调用就必须最终降至0。因此N必须大于等于1,这就是阶乘类型的
来由。这一点非常巧妙。

 4.使用dialyzer

用dialyzer来检查程序里的类型错误需要按照一种特定的工作流进行。不要不加任何类型注解就编写完整个程序,然后当你感觉一切就绪后再回过头来到处添加类型注解并运行dialyzer。这么做,很可能会见到大量让人困惑的错误,从而不知道该从哪里着手修复错误。使用dialyzer的最佳方式是将它用于每一个开发阶段。开始编写一个新模块时,应该首先考虑类型并声明它们,然后再编写代码。要为模块里所有的导出函数编写类型规范。先完成这一步再开始写代码。可以先注释掉未完成函数的类型规范,然后在实现函数的过程中取消注释。现在可以开始编写函数了,一次写一个,每写完一个新函数就要用dialyzer检查一下,看看能否发现程序错误。如果函数是导出的,就加上类型规范;如果不是,就要考虑添加类型规范是否有助于类型分析或者能帮助我们理解程序(请记住,类型注解是很好的程序文档)。如果dialyzer发现了任何错误,就应该停下来思考并找出错误的准确含义。

5.干扰dialyzer的事物

dialyzer很容易受到干扰。我们可以通过遵循几条简单的规则来防止这种情况发生:

(1)避免使用-compile(export_all) 。如果导出了模块里的所有函数, dialyzer 就可能无法
推理出某些导出函数的参数,因为这些函数的调用位置和类型可能会千变万化。这些参
数的值可能会扩散到模块的其他函数,导致让人困惑的错误。
(2)为模块 导出 函数的所有参数提供详细的类型规范。尽量给导出函数的参数设置最严格的
限制。举个例子,乍看之下你可能会推断函数的某个参数是一个整数,但经过进一步思
考,也许就能确定该参数是一个正整数,甚至位于某个范围之内。把类型设置得越精确,
dialyzer 的分析结果就越出色。另外,如果可能,你还应该为代码添加精确的关卡测试。
这会有助于程序分析,而且往往能帮助编译器生成质量更高的代码。
(3)为记录定义里的所有元素提供默认的参数。如果不提供,原子 undefined 就会被当成默认值,而这个类型会逐渐扩散到程序的其他部分,可能导致奇特的类型错误。
(4)把匿名变量用作函数的参数经常会导致结果类型不如你预想得那么精确。要尽可能地给
变量添加限制。

四: 类型推断与成功分型

dialyzer 生成的某些错误十分奇特。要理解这些错误,必须理解 dialyzer得出Erlang 函数类型的过程。理解它能帮助我们解读这些古怪的错误消息。类型推断(type inference )是指通过分析代码得出函数类型的过程。要做到这一点,我们会分析程序,寻找约束条件 。用这些约束条件构建出一组约束方程式,然后求解。得到的一组类型就被称为此程序的成功分型 success typing )。如:
%% dialyzer/types1.erl
-module(types1).
-export([f1/1, f2/1, f3/1]).

f1(H,M,S})->
    (H+M*60)*60+S.

f2({H,M,S}) when is_integer(H) ->
    (H+M*60)*60+S.

f3({H,M.S}) ->
    print(H, M, S),
    (H+M*60)*60+S.

print(H,M,S)->
    Str = integer_to_list(H) ++ ":" ++ integer_to_list(M) ++ ":" ++
    integer_to_list(S),
    io:format("~s",[Str]).
可以观察上面的代码,试着找出代码里各个变量的类型。这样你才能通过运行行dialyzer理解类型推断与成功分型,下面是运行 dialyzer之后所发生的事:
$ dialyzer typesl.erl
Checking whether the PLT /Users/joe/.dialyzer_plt is up-to-date...yes
Proceeding with analysis...done in 0m0.41s
done (passed successfully)
dialyzer 在这段代码里没有找到类型错误。但这并不意味着代码就是正确的,它仅仅是指程序里所有数据类型的使用方式相互一致。我在把时、分、秒转换成秒时写的是(H+M*60)*60+S ,而这是完全错误的,应该是(H*60+M)*60+S。没有任何类型系统能检测出这一点。所以即使程序具备正确的类型,仍然需要对其进行案例测试。在这个程序上运行 typer 会产生以下输出:
$ typer typesl.erl
%% File:"types1.erl"
%% --------------------------------------
-spec f1({number(), number(), number()}) -> number().
-spec f2({integer(), number(), number()}) -> number().
-spec f3({integer(), integer(), integer()}) -> integer().
-spec print(integer(), integer(), integer()) -> 'ok'.
typer 会报告它分析的模块里所有函数的类型。 typer 将函数 f1 的类型说明如下:
-spec f1({number(),number(),number()})->number().
这可以通过观察 f1 的定义得出,它的定义如下:
f1({H, M, S}) ->
    (H+M*60)*60+S.
这个函数为我们提供了 5 个不同的约束条件。首先, f1 的参数必须是一个包含三个元素的元组。其次,每个算术操作符都增加了一个约束条件。比如,子表达式 M*60 告诉我们 M 必须属于number()类型,因为乘法操作符的左右参数都必须是数字。类似地, ...+S 也告诉我们 S 必须是一个数字。现在来看看函数f2。下面列出了它的代码和推断类型:
f2({H,M,S}) when is_integer(H) ->
    (H+M*60)*60+S.
-spec f2({integer(), number(), number()}) -> number()
添加的关卡 is_integer(H) 是一个额外的约束条件,即 H 必须是一个整数。这个约束条件 变了f2 元组参数里第一个元素的类型,把它从 number() 改成了更精确的 integer() 类型。
注:这里严谨的说法应该是“添加了额外的约束条件,因此,如果该函数能正常工作,H就必然是一个整数”。这就是我们把函数的推断类型称为合格类型的原因,从字面上讲就是“要让函数能成功执行,它的参数就必须属于这个类型”。
现在来看 types1.erl 的最后一个函数:
f3({H,M.S}) ->
    print(H, M, S),
    (H+M*60)*60+S.

print(H,M,S)->
    Str = integer_to_list(H) ++ ":" ++ integer_to_list(M) ++ ":" ++
    integer_to_list(S),
    io:format("~s",[Str]).
它的推断类型如下:
-spec f3({integer(), integer(), integer()}) -> integer().
-spec print(integer(), integer(), integer()) -> 'ok'.
这里你就能看到对 integer_to_list 的调用是如何把它的参数限制成一个整数的。随后,这个出现在print 函数里的约束条件扩散到了 f3 函数的主体中。
如你所见,类型分析的过程有两个阶段。首先会得出一组约束方程式,然后进行求解。如果dialyzer没有找到错误,就表明这组约束方程式有解的, typer 则会打印出这些方程的解。如果
这些方程式存在不一致性,无法求解, dialyzer 会报告一个错误
如果我们在之前的程序中引入一个错误,改动如下:
%% dialyzer/types1.erl
-module(types1).
-export([f1/1, f2/1, f3/1, f4/1]).

%% f1(H,M,S})->
%%    (H+M*60)*60+S.

%% f2({H,M,S}) when is_integer(H) ->
%%    (H+M*60)*60+S.

%% f3({H,M.S}) ->
%%    print(H, M, S),
%%    (H+M*60)*60+S.

%% 改动地方
f4({H,M,S}) when is_float(H)->
    print(H,M,S),
    (H+M*60)*60+S.

print(H,M,S)->
    Str = integer_to_list(H) ++ ":" ++ integer_to_list(M) ++ ":" ++
    integer_to_list(S),
    io:format("~s",[Str]).
首先运行 typer,结果为:
$ typer types1_bug.erl
-spec f4(_) -> none().
-spec print(integer(), integer(), integer()) -> 'ok'.
typer 报告说 f4 返回类型 none() 。这个特殊类型的意思是“此函数永远不会返回”。运行 dialyzer 时会看到以下输出:
$ dialyzer types1_bug.erl
typesl_bug.erl:4: Function f4/1 has no local return
types1_bug.erl:5: The call types1_bug:print(H :: float(),M :: any(),S :: any())
        will never return since it differs in the lst argument from the
        success typing arguments:(integer(), integer(), integer())
types1_bug.erl:8: Function print/3 has no local return
types1_bug.erl:9: The call erlang:integer_to_list(H:float())
        will never return since it differs in the 1st argument from the
        success typing arguments:(integer())
现在回过头来观察一下代码。关卡测试 is_float(H) 告诉系统 H 必然是一个浮点数。而随着 H 扩散print 函数, print 内部的函数调用 integer_to_list(H) 却告诉系统 H 必然是一个整数。现在dialyzer无法确定这两种陈述哪一种才是正确的因此假定它们都是错误的。这就是它报告“ Function print/3 has no local return value” print/3 函数没有本地返回值)的原因。这是类型系统的局限性之一,它们能报告的就是程序存在不一致性,然后把问题留给程序员来分析解决。

五:类型系统的局限性

我们将从众所周知的布尔函数 and开始。and只有在它的两个参数都为true时才为true,如果任何一个参数是false,它的值就是false。定义一个myand1函数(它的工作方式应该和and一样)如下:
%% types1.erl
myandl(true,true) -> true;
myand1(false,_) -> false;
myandl(_,false) -> false.

对它运行typer后可得到以下输出:

$ typer typesl.erl
-spec myandl(_,_) -> boolean().
...
myand1 的推断类型是 (_,_) -> boolean() ,意思是 myand1 的各个参数都可以是任何你喜
欢的值,返回的类型则是 boolean myand1 的参数可以是任何值”这个推断依据的是参数位置
里的下划线。比如, myand1 的子句 2 myand1(false, _) -> false ,基于此它推断出第二个
参数可以是任何值。现在,假设给这个模块添加一个错误的 bug1 函数如下:
%% types1.erl
myandl(true,true) -> true;
myand1(false,_) -> false;
myandl(_,false) -> false.

bug1(X,Y)->
    case myand1(X, Y) of
        true ->
            X + Y
    end.
然后让 typer 分析这个模块:
$ typer typesl.erl
-spec myandl(_,_) -> boolean()
-spec bugl(number(), number()) -> number().
typer 知道 + 用两个数字作为参数并返回一个数字,因此推断 XY 都是数字。它还推断出
myand1的参数可以是任何值,这与XY 都是数字不矛盾。如果在这个模块上运行 dialyzer ,它是
不会返回错误的。 typer认为用两个数字参数调用bug1 会返回一个数字但它不会它会崩溃
这个例子展示了参数类型规范的不到位(即把 _ 当作类型而非 boolean() )会导致分析程序时无
法发现的错误。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如皓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值