檢視原始碼 Funs

map

以下函式 double 會將列表中的每個元素加倍

double([H|T]) -> [2*H|double(T)];
double([])    -> [].

因此,輸入的參數會如下加倍

> double([1,2,3,4]).
[2,4,6,8]

以下函式 add_one 會將列表中的每個元素加一

add_one([H|T]) -> [H+1|add_one(T)];
add_one([])    -> [].

函式 doubleadd_one 具有相似的結構。可以透過編寫函式 map 來表達這種相似性

map(F, [H|T]) -> [F(H)|map(F, T)];
map(F, [])    -> [].

函式 doubleadd_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. 步驟 1. 編寫一個函式來表示這些函式的共同特徵。
  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 不定義其元素的處理順序。

Funs 的語法

Funs 的編寫語法如下(完整描述請參閱函式表達式

F = fun (Arg1, Arg2, ... ArgN) ->
        ...
    end

這會建立一個具有 N 個參數的匿名函式,並將其繫結到變數 F

可以使用以下語法傳遞在同一模組中編寫的另一個函式 FunctionName 作為參數

F = fun FunctionName/Arity

使用這種形式的函式參照,被參照的函式不需要從模組中匯出。

也可以使用以下語法參照在不同模組中定義的函式

F = fun Module:FunctionName/Arity

在這種情況下,函式必須從相關模組匯出。

以下程式說明建立 funs 的不同方式

-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.

可以使用以下語法評估 fun F

F(Arg1, Arg2, ..., Argn)

若要檢查術語是否為 fun,請在防護中使用測試 is_function/1

範例

f(F, Args) when is_function(F) ->
   apply(F, Args);
f(N, _) when is_integer(N) ->
   N.

Funs 是一種獨特的類型。BIF erlang:fun_info/1,2 可用於擷取有關 fun 的資訊,而 BIF erlang:fun_to_list/1 會傳回 fun 的文字表示形式。check_process_code/2 BIF 如果程序包含依賴於模組舊版本的 fun,則傳回 true

Fun 內的變數繫結

fun 中出現的變數範圍規則如下

  • fun 標頭中出現的所有變數都假設為「新」變數。
  • 在 fun 之前定義,並且在 fun 內的函式呼叫或防護測試中出現的變數,具有它們在 fun 外部的值。
  • 變數無法從 fun 匯出。

以下範例說明這些規則

print_list(File, List) ->
    {ok, Stream} = file:open(File, write),
    foreach(fun(X) -> io:format(Stream,"~p~n",[X]) end, List),
    file:close(Stream).

在這裡,在 fun 的標頭中定義的變數 X 是一個新變數。在 fun 中使用的變數 Stream 會從 file:open 行取得其值。

由於 fun 的標頭中出現的任何變數都被視為新變數,因此以下寫法同樣有效

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。這不是明智之舉,因為 fun 主體中的程式碼無法參照在 fun 外部定義的變數 File。編譯此範例會產生以下診斷

./FileName.erl:Line: Warning: variable 'File'
      shadowed in 'fun'

這表示在 fun 內部定義的變數 File 與在 fun 外部定義的變數 File 衝突。

將變數匯入 fun 的規則導致某些模式比對操作必須移至防護表達式,並且無法在 fun 的標頭中編寫。例如,如果您希望在參數值為 Y 時評估 F 的第一個子句,您可能會編寫以下程式碼

f(...) ->
    Y = ...
    map(fun(X) when X == Y ->
             ;
           (_) ->
             ...
        end, ...)
    ...

而不是編寫以下程式碼

f(...) ->
    Y = ...
    map(fun(Y) ->
             ;
           (_) ->
             ...
        end, ...)
    ...

Funs 和模組列表

以下範例顯示與 Erlang Shell 的對話。所有討論的高階函式都從模組 lists 匯出。

map

lists:map/2 接收一個帶有一個參數的函式和一個術語列表

map(F, [H|T]) -> [F(H)|map(F, T)];
map(F, [])    -> [].

它傳回通過將該函式應用於列表中的每個參數所取得的列表。

當在 Shell 中定義新的 fun 時,fun 的值會列印為 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

lists:any/2 接收一個帶有一個參數的謂詞 P 和一個術語列表

any(Pred, [H|T]) ->
    case Pred(H) of
        true  ->  true;
        false ->  any(Pred, T)
    end;
any(Pred, []) ->
    false.

謂詞是一個傳回 truefalse 的函式。如果列表中有一個術語 X 使得 P(X)true,則 anytrue

定義了一個謂詞 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

lists:all/2 具有與 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

lists:foreach/2 接收一個帶有一個參數的函式和一個術語列表

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

lists:foldl/3 接收一個帶有兩個參數的函式、一個累加器和一個列表

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

lists:foldl/3 的工作方式類似於命令式語言中的 while 迴圈

L =  ["I","like","Erlang"],
Sum = 0,
while( L != []){
    Sum += length(head(L)),
    L = tail(L)
end

mapfoldl

lists:mapfoldl/3 同時對列表進行 map 和 fold

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"]

現在,可以同時完成 fold 和 map

> lists:mapfoldl(fun(Word, Sum) ->
{Upcase_word(Word), Sum + length(Word)}
end, 0, L).
{["I","LIKE","ERLANG"],11}

filter

lists:filter/2 接收一個帶有一個參數的謂詞和一個列表,並傳回列表中所有符合該謂詞的元素

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]

結合 map 和 filter 可編寫非常簡潔的程式碼。例如,若要將集合差分函式 diff(L1, L2) 定義為列表 L1L2 之間的差,程式碼可以編寫如下

diff(L1, L2) ->
    filter(fun(X) -> not member(X, L2) end, L1).

這會提供 L1 中未包含在 L2 中的所有元素的列表。

列表 L1L2 的 AND 交集也很容易定義

intersection(L1,L2) -> filter(fun(X) -> member(X,L1) end, L2).

takewhile

只要謂詞 P(X) 為 true,lists:takewhile/2 就會從列表 L 中取得元素 X

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

lists:dropwhile/2takewhile 的補數

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

lists:splitwith/2 將列表 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]}

傳回 Funs 的 Funs

到目前為止,僅說明了將 funs 作為參數的函式。也可以編寫更強大的函式,這些函式本身會傳回 funs。以下範例說明這些類型的函式。

簡單的高階函式

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 是一個解析樹。
  • Toks1Tree 的尾部,包含正確解析的結構之後遇到的符號。

不成功的解析會返回 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

接下來,定義了兩個高階函數 pandpor。它們結合了原始解析器以產生更複雜的解析器。

首先是 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 的解析器 P2pand(P1, P2) 返回一個文法的解析器,該文法由滿足 G1 的符號序列,後跟滿足 G2 的符號序列組成。

por(P1, P2) 返回一個由文法 G1G2 描述的語言的解析器

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