2 函数
2.1 map
以下函数 double 将列表中的每个元素都加倍
double([H|T]) -> [2*H|double(T)]; double([]) -> [].
因此,输入的实参会被加倍,如下所示
> double([1,2,3,4]).
[2,4,6,8]
以下函数 add_one 将列表中的每个元素都加 1
add_one([H|T]) -> [H+1|add_one(T)]; add_one([]) -> [].
函数 double 和 add_one 的结构相似。这可以通过编写一个表示这种相似性的函数 map 来实现
map(F, [H|T]) -> [F(H)|map(F, T)]; map(F, []) -> [].
现在,函数 double 和 add_one 可以用 map 来表示,如下所示
double(L) -> map(fun(X) -> 2*X end, L). add_one(L) -> map(fun(X) -> 1 + X end, L).
map(F, List) 是一个函数,它以一个函数 F 和一个列表 L 作为实参,并返回一个新的列表,该列表是通过将 F 应用于 L 中的每个元素而获得的。
从多个不同程序的共同特征中抽象出公共特征的过程称为 **过程抽象**。过程抽象可以用来编写多个具有相似结构但细节略有不同的函数。这可以通过以下步骤完成
- **步骤 1.** 编写一个表示这些函数的共同特征的函数。
- **步骤 2.** 将差异参数化为作为实参传递给公共函数的函数。
2.2 foreach
本节说明了过程抽象。最初,以下两个示例被编写为传统的函数。
此函数将列表中的所有元素打印到流上
print_list(Stream, [H|T]) -> io:format(Stream, "~p~n", [H]), print_list(Stream, T); print_list(Stream, []) -> true.
此函数将消息广播到进程列表
broadcast(Msg, [Pid|Pids]) -> Pid ! Msg, broadcast(Msg, Pids); broadcast(_, []) -> true.
这两个函数的结构相似。它们都遍历一个列表,并对列表中的每个元素执行某些操作。这个“某些操作”被作为额外的实参传递给执行此操作的函数。
函数 foreach 表达了这种相似性
foreach(F, [H|T]) -> F(H), foreach(F, T); foreach(F, []) -> ok.
使用函数 foreach,函数 print_list 变为
foreach(fun(H) -> io:format(S, "~p~n",[H]) end, L)
使用函数 foreach,函数 broadcast 变为
foreach(fun(Pid) -> Pid ! M end, L)
foreach 是为了其副作用而不是其值而被求值的。foreach(Fun ,L) 为 L 中的每个元素 X 调用 Fun(X),并且处理过程按照元素在 L 中定义的顺序进行。 map 没有定义其元素的处理顺序。
2.3 函数语法
函数使用以下语法编写(有关完整描述,请参阅 函数表达式 )
F = fun (Arg1, Arg2, ... ArgN) -> ... end
这将创建一个具有 N 个实参的匿名函数,并将其绑定到变量 F。
另一个函数 FunctionName,在同一个模块中编写,可以使用以下语法作为实参传递
F = fun FunctionName/Arity
使用这种函数引用的形式,引用的函数不需要从模块中导出。
也可以使用以下语法引用在不同模块中定义的函数
F = fun Module:FunctionName/Arity
在这种情况下,函数必须从相关的模块中导出。
以下程序说明了创建函数的不同方法
-module(fun_test). -export([t1/0, t2/0]). -import(lists, [map/2]). t1() -> map(fun(X) -> 2 * X end, [1,2,3,4,5]). t2() -> map(fun double/1, [1,2,3,4,5]). double(X) -> X * 2.
函数 F 可以使用以下语法求值
F(Arg1, Arg2, ..., Argn)
要检查一个项是否为函数,请在保护中使用测试 is_function/1。
示例
f(F, Args) when is_function(F) -> apply(F, Args); f(N, _) when is_integer(N) -> N.
函数是一种独特的类型。BIF erlang:fun_info/1,2 可以用来检索有关函数的信息,而 BIF erlang:fun_to_list/1 返回函数的文本表示。 check_process_code/2 BIF 在进程包含依赖于旧版模块的函数时返回 true。
2.4 函数内的变量绑定
函数中出现的变量的范围规则如下
- 函数头部中出现的变量都被认为是“新”变量。
- 在函数之前定义的变量,以及在函数内的函数调用或保护测试中出现的变量,将具有它们在函数外部的值。
- 变量不能从函数中导出。
以下示例说明了这些规则
print_list(File, List) -> {ok, Stream} = file:open(File, write), foreach(fun(X) -> io:format(Stream,"~p~n",[X]) end, List), file:close(Stream).
这里,在函数头部定义的变量 X 是一个新的变量。在函数内部使用的变量 Stream 获取其来自 file:open 行的值。
由于函数头部中出现的任何变量都被视为新的变量,因此以以下方式编写也是有效的
print_list(File, List) -> {ok, Stream} = file:open(File, write), foreach(fun(File) -> io:format(Stream,"~p~n",[File]) end, List), file:close(Stream).
这里,File 被用作新的变量,而不是 X。这样做不太明智,因为函数主体内的代码无法引用在函数外部定义的变量 File。编译此示例会给出以下诊断信息
./FileName.erl:Line: Warning: variable 'File' shadowed in 'fun'
这表明在函数内部定义的变量 File 与在函数外部定义的变量 File 发生冲突。
将变量导入函数的规则导致某些模式匹配操作必须移入保护表达式,而不能在函数头部编写。例如,如果你打算在实参的值为 Y 时对 F 的第一个子句进行求值,你可能会编写以下代码
f(...) -> Y = ... map(fun(X) when X == Y -> ; (_) -> ... end, ...) ...
而不是编写以下代码
f(...) -> Y = ... map(fun(Y) -> ; (_) -> ... end, ...) ...
2.5 函数和模块列表
以下示例显示了与 Erlang shell 的对话。所有讨论的高阶函数都从模块 lists 中导出。
map
map 接收一个带有一个实参的函数和一个项列表
map(F, [H|T]) -> [F(H)|map(F, T)]; map(F, []) -> [].
它返回通过将函数应用于列表中的每个实参而获得的列表。
当在 shell 中定义一个新的函数时,函数的值将被打印为 Fun#<erl_eval>
> Double = fun(X) -> 2 * X end. #Fun<erl_eval.6.72228031> > lists:map(Double, [1,2,3,4,5]). [2,4,6,8,10]
any
any 接收一个带有一个实参的谓词 P 和一个项列表
any(Pred, [H|T]) -> case Pred(H) of true -> true; false -> any(Pred, T) end; any(Pred, []) -> false.
谓词是一个返回 true 或 false 的函数。 如果列表中存在一个项 X 使得 P(X) 为 true,那么 any 为 true。
定义了一个谓词 Big(X),当其实参大于 10 时,它为 true
> Big = fun(X) -> if X > 10 -> true; true -> false end end. #Fun<erl_eval.6.72228031> > lists:any(Big, [1,2,3,4]). false > lists:any(Big, [1,2,3,12,5]). true
all
all 与 any 的实参相同
all(Pred, [H|T]) -> case Pred(H) of true -> all(Pred, T); false -> false end; all(Pred, []) -> true.
如果应用于列表中所有元素的谓词都为 true,那么它为 true。
> lists:all(Big, [1,2,3,4,12,6]). false > lists:all(Big, [12,13,14,15]). true
foreach
foreach 接收一个带有一个实参的函数和一个项列表
foreach(F, [H|T]) -> F(H), foreach(F, T); foreach(F, []) -> ok.
函数将被应用于列表中的每个实参。 foreach 返回 ok。它仅用于其副作用
> lists:foreach(fun(X) -> io:format("~w~n",[X]) end, [1,2,3,4]).
1
2
3
4
ok
foldl
foldl 接收一个带两个实参的函数、一个累加器和一个列表
foldl(F, Accu, [Hd|Tail]) -> foldl(F, F(Hd, Accu), Tail); foldl(F, Accu, []) -> Accu.
函数将被调用两次。第一个实参是列表中的后续元素。第二个实参是累加器。函数必须返回一个新的累加器,该累加器将在下次调用函数时使用。
如果你有一个列表列表 L = ["I","like","Erlang"],那么你可以将 L 中所有字符串的长度加起来,如下所示
> L = ["I","like","Erlang"]. ["I","like","Erlang"] 10> lists:foldl(fun(X, Sum) -> length(X) + Sum end, 0, L). 11
foldl 的工作方式类似于命令式语言中的 while 循环
L = ["I","like","Erlang"], Sum = 0, while( L != []){ Sum += length(head(L)), L = tail(L) end
mapfoldl
mapfoldl 同时对列表进行映射和折叠
mapfoldl(F, Accu0, [Hd|Tail]) -> {R,Accu1} = F(Hd, Accu0), {Rs,Accu2} = mapfoldl(F, Accu1, Tail), {[R|Rs], Accu2}; mapfoldl(F, Accu, []) -> {[], Accu}.
以下示例展示了如何将 L 中的所有字母更改为大写,然后对其进行计数。
首先更改为大写
> Upcase = fun(X) when $a =< X, X =< $z -> X + $A - $a; (X) -> X end. #Fun<erl_eval.6.72228031> > Upcase_word = fun(X) -> lists:map(Upcase, X) end. #Fun<erl_eval.6.72228031> > Upcase_word("Erlang"). "ERLANG" > lists:map(Upcase_word, L). ["I","LIKE","ERLANG"]
现在,折叠和映射可以同时完成
> lists:mapfoldl(fun(Word, Sum) -> {Upcase_word(Word), Sum + length(Word)} end, 0, L). {["I","LIKE","ERLANG"],11}
filter
filter 接收一个带有一个实参的谓词和一个列表,并返回列表中满足谓词的所有元素
filter(F, [H|T]) -> case F(H) of true -> [H|filter(F, T)]; false -> filter(F, T) end; filter(F, []) -> [].
> lists:filter(Big, [500,12,2,45,6,7]).
[500,12,45]
结合映射和过滤器可以编写非常简洁的代码。例如,要定义一个集合差函数 diff(L1, L2) 来表示列表 L1 和 L2 之间的差,可以编写以下代码
diff(L1, L2) -> filter(fun(X) -> not member(X, L2) end, L1).
这将给出 L1 中所有不在 L2 中的元素的列表。
列表 L1 和 L2 的 AND 交集也很容易定义
intersection(L1,L2) -> filter(fun(X) -> member(X,L1) end, L2).
takewhile
takewhile(P, L) 从列表 L 中获取元素 X,只要谓词 P(X) 为 true
takewhile(Pred, [H|T]) -> case Pred(H) of true -> [H|takewhile(Pred, T)]; false -> [] end; takewhile(Pred, []) -> [].
> lists:takewhile(Big, [200,500,45,5,3,45,6]).
[200,500,45]
dropwhile
dropwhile 是 takewhile 的补集
dropwhile(Pred, [H|T]) -> case Pred(H) of true -> dropwhile(Pred, T); false -> [H|T] end; dropwhile(Pred, []) -> [].
> lists:dropwhile(Big, [200,500,45,5,3,45,6]).
[5,3,45,6]
splitwith
splitwith(P, L) 将列表 L 分割成两个子列表 {L1, L2},其中 L = takewhile(P, L) 并且 L2 = dropwhile(P, L)
splitwith(Pred, L) -> splitwith(Pred, L, []). splitwith(Pred, [H|T], L) -> case Pred(H) of true -> splitwith(Pred, T, [H|L]); false -> {reverse(L), [H|T]} end; splitwith(Pred, [], L) -> {reverse(L), []}.
> lists:splitwith(Big, [200,500,45,5,3,45,6]).
{[200,500,45],[5,3,45,6]}
2.6 函数返回函数
到目前为止,我们只描述了以函数作为实参的函数。还可以编写更强大的函数,它们本身返回函数。以下示例说明了这些类型的函数。
简单的高阶函数
Adder(X) 是一个函数,它在给定 X 的情况下,返回一个新的函数 G,使得 G(K) 返回 K + X
> Adder = fun(X) -> fun(Y) -> X + Y end end. #Fun<erl_eval.6.72228031> > Add6 = Adder(6). #Fun<erl_eval.6.72228031> > Add6(10). 16
无限列表
想法是写类似以下内容
-module(lazy). -export([ints_from/1]). ints_from(N) -> fun() -> [N|ints_from(N+1)] end.
然后按照以下步骤进行
> XX = lazy:ints_from(1). #Fun<lazy.0.29874839> > XX(). [1|#Fun<lazy.0.29874839>] > hd(XX()). 1 > Y = tl(XX()). #Fun<lazy.0.29874839> > hd(Y()). 2
等等。这是一个“延迟嵌入”的例子。
解析
以下示例展示了以下类型的解析器
Parser(Toks) -> {ok, Tree, Toks1} | fail
Toks 是要解析的标记列表。成功的解析将返回 {ok, Tree, Toks1}。
- Tree 是解析树。
- Toks1 是 Tree 的尾部,它包含在正确解析的结构之后遇到的符号。
不成功的解析将返回 fail。
以下示例说明了一个简单的函数式解析器,它解析以下语法
(a | b) & (c | d)
以下代码在模块 funparse 中定义了一个函数 pconst(X),它返回一个解析标记列表的函数
pconst(X) -> fun (T) -> case T of [X|T1] -> {ok, {const, X}, T1}; _ -> fail end end.
此函数可以按以下方式使用
> P1 = funparse:pconst(a). #Fun<funparse.0.22674075> > P1([a,b,c]). {ok,{const,a},[b,c]} > P1([x,y,z]). fail
接下来,定义了两个高阶函数 pand 和 por。它们将原始解析器组合在一起以生成更复杂的解析器。
首先是 pand
pand(P1, P2) -> fun (T) -> case P1(T) of {ok, R1, T1} -> case P2(T1) of {ok, R2, T2} -> {ok, {'and', R1, R2}}; fail -> fail end; fail -> fail end end.
给定一个用于语法 G1 的解析器 P1 和一个用于语法 G2 的解析器 P2,pand(P1, P2) 返回一个用于语法的解析器,该语法由满足 G1 的标记序列,后跟满足 G2 的标记序列组成。
por(P1, P2) 返回一个解析器,用于解析由语法 G1 或 G2 描述的语言
por(P1, P2) -> fun (T) -> case P1(T) of {ok, R, T1} -> {ok, {'or',1,R}, T1}; fail -> case P2(T) of {ok, R1, T1} -> {ok, {'or',2,R1}, T1}; fail -> fail end end end.
原始问题是解析语法 (a | b) & (c | d)。以下代码解决了这个问题
grammar() -> pand( por(pconst(a), pconst(b)), por(pconst(c), pconst(d))).
以下代码为语法添加了一个解析器接口
parse(List) -> (grammar())(List).
解析器可以按如下方式测试
> funparse:parse([a,c]). {ok,{'and',{'or',1,{const,a}},{'or',1,{const,c}}}} > funparse:parse([a,d]). {ok,{'and',{'or',1,{const,a}},{'or',2,{const,d}}}} > funparse:parse([b,c]). {ok,{'and',{'or',2,{const,b}},{'or',1,{const,c}}}} > funparse:parse([b,d]). {ok,{'and',{'or',2,{const,b}},{'or',2,{const,d}}}} > funparse:parse([a,b]). fail