前言:
一:指定数据和函数类型
%% 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类型构成的列表
1> walks:p1 an route({1,10},{25,57}).
[{go, east, 24},
{go, north, 47},
...
]
-spec planroute(From :: point(), To :: point()) -> ...
-type angle() :: {Degrees :: 0..360, Minutes :: 0..60, Seconds :: 0..60}.
-type position() :: {latitude | longitude, angle()}.
-spec plan_routel(From :: position(), To :: position()) -> ...
二:Erlang 的类型表示法
1.类型的语法
T1 :: A | B | C ...
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}
-type NewTypeName(TVar1, TVar2, ..., 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}].
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().
3.指定函数的输入输出类型
-spec functionName(T1, T2, ..., Tn) -> Tret when
Ti :: Typei,
Tj :: Typej,
...
-spec file:open(FileName,Modes) -> {ok,Handle} | {error,Why} when
FileName :: string(),
Modes :: [Mode],
Mode :: read | write | ...
Handle :: file_handle(),
why :: error_term().
-spec file:open(string(), [read | write | ...] -> {ok,Handle} | {error,Why}
-spec lists :: map(fun((A) -> B), [A]) -> [B].
-spec lists :: filter(fun((X) -> bool()), [X]) -> [X].
4.导出类型和本地类型
-module(a).
-type rich_text() :: [{font(), char()}].
-type font() :: integer().
-export_type([rich_text/0, font/0]).
...
-module(b).
...
-spec rich_text_length(a:rich_text()) -> integer().
...
...
5.不透明类型
-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()}].
-module(b).
...
do this() ->
X = a:make_text("hello world"),
{W,H} = a:bounding_box(X).
-module(c).
...
fonts_in(Str) ->
X = a:make_text(Str),
[F || {F, _} <- X].
三: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需要花费几分钟的时间。我们输入的第一个命令会生成erts、stdlib和kernel的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
2.内置函数的错误参数
%% 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/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().
型是none()。
4.使用dialyzer
用dialyzer来检查程序里的类型错误需要按照一种特定的工作流进行。不要不加任何类型注解就编写完整个程序,然后当你感觉一切就绪后再回过头来到处添加类型注解并运行dialyzer。这么做,很可能会见到大量让人困惑的错误,从而不知道该从哪里着手修复错误。使用dialyzer的最佳方式是将它用于每一个开发阶段。开始编写一个新模块时,应该首先考虑类型并声明它们,然后再编写代码。要为模块里所有的导出函数编写类型规范。先完成这一步再开始写代码。可以先注释掉未完成函数的类型规范,然后在实现函数的过程中取消注释。现在可以开始编写函数了,一次写一个,每写完一个新函数就要用dialyzer检查一下,看看能否发现程序错误。如果函数是导出的,就加上类型规范;如果不是,就要考虑添加类型规范是否有助于类型分析或者能帮助我们理解程序(请记住,类型注解是很好的程序文档)。如果dialyzer发现了任何错误,就应该停下来思考并找出错误的准确含义。
5.干扰dialyzer的事物
dialyzer很容易受到干扰。我们可以通过遵循几条简单的规则来防止这种情况发生:
(1)避免使用-compile(export_all) 。如果导出了模块里的所有函数, dialyzer 就可能无法推理出某些导出函数的参数,因为这些函数的调用位置和类型可能会千变万化。这些参数的值可能会扩散到模块的其他函数,导致让人困惑的错误。(2)为模块 导出 函数的所有参数提供详细的类型规范。尽量给导出函数的参数设置最严格的限制。举个例子,乍看之下你可能会推断函数的某个参数是一个整数,但经过进一步思考,也许就能确定该参数是一个正整数,甚至位于某个范围之内。把类型设置得越精确,dialyzer 的分析结果就越出色。另外,如果可能,你还应该为代码添加精确的关卡测试。这会有助于程序分析,而且往往能帮助编译器生成质量更高的代码。(3)为记录定义里的所有元素提供默认的参数。如果不提供,原子 undefined 就会被当成默认值,而这个类型会逐渐扩散到程序的其他部分,可能导致奇特的类型错误。(4)把匿名变量用作函数的参数经常会导致结果类型不如你预想得那么精确。要尽可能地给变量添加限制。
四: 类型推断与成功分型
%% 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 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)
$ 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'.
-spec f1({number(),number(),number()})->number().
f1({H, M, S}) ->
(H+M*60)*60+S.
f2({H,M,S}) when is_integer(H) ->
(H+M*60)*60+S.
-spec f2({integer(), number(), number()}) -> number()
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'.
%% 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 types1_bug.erl
-spec f4(_) -> none().
-spec print(integer(), integer(), integer()) -> 'ok'.
$ 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())
五:类型系统的局限性
%% types1.erl
myandl(true,true) -> true;
myand1(false,_) -> false;
myandl(_,false) -> false.
对它运行typer后可得到以下输出:
$ typer typesl.erl
-spec myandl(_,_) -> boolean().
...
%% 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 typesl.erl
-spec myandl(_,_) -> boolean()
-spec bugl(number(), number()) -> number().