cv.cm

編碼 · 程式語言

Erlang 入門教學

閱讀約 9 分鐘

簡答

Erlang 是愛立信為必須持續運作的系統(如電話交換機與訊息伺服器)所創造的函數式並行語言。程式運行在 BEAM 虛擬機上,由許多輕量行程組成,行程之間以訊息溝通,失敗時由 supervisor 重新啟動。本頁涵蓋編譯模組、型別、模式比對、遞迴、串列與 map、行程、OTP 與錯誤處理。

Erlang 是什麼、可以用來做什麼

Erlang 是一種函數式、動態型別的語言,1980 年代由愛立信為必須不間斷運作的電話交換機而設計,1998 年以開放原始碼釋出。Erlang 程式運行在 BEAM 虛擬機上,由許多互不共享狀態、以訊息溝通的輕量行程組成,並附有 OTP(Open Telecom Platform),這是一套用來建構容錯系統的函式庫與設計原則。

Erlang 用於電信設備、訊息平台(WhatsApp 的後端就是以 Erlang 打造而聞名)、RabbitMQ 等訊息代理、Riak 與 CouchDB 等資料庫,以及支付與物聯網系統。Elixir 與 Gleam 也運行在 BEAM 上,可直接使用 Erlang 的函式庫。

安裝 Erlang 並執行 Hello, world

從 erlang.org 或透過套件管理工具安裝 Erlang/OTP(例如 brew install erlang 或 apt install erlang)。程式碼放在模組中;每個 name.erl 檔案定義模組 name,並列出它匯出的函式。

% hello.erl
-module(hello).
-export([main/0]).

main() ->
    io:format("Hello, world!~n").
erlc hello.erl                          # compiles to hello.beam
erl -noshell -s hello main -s init stop
# Hello, world!
erl                                     # interactive shell; c(hello). then hello:main().

註解以 % 開頭。格式字串中的 ~n 代表換行,~p 會印出任何 term。在 shell 中,每個運算式都以句點結尾。

標點符號、變數與基本型別

標點符號是 Erlang 帶給初學者最大的驚訝:逗號分隔運算式,分號分隔子句,句點則結束一個函式或 shell 指令。變數以大寫字母或底線開頭,而且只能綁定一次。

1> Name = "Erlang".
"Erlang"
2> Year = 1986, Next = Year + 1.
1987
3> Year = 2000.
** exception error: no match of right hand side value 2000
4> 10 / 4.
2.5
5> {10 div 4, 10 rem 4}.
{2,2}
6> is_atom(ok).
true
7> <<"binary string">>.
<<"binary string">>
型別範例說明
integer42, 16#FF任意精度;十六進位用 16#
float3.1464 位元雙精度
atomok, error, 'Hello'有名稱的常數,小寫開頭或加引號
tuple{ok, 42}大小固定的一組值
list[1, 2, 3]鏈結串列;像 "abc" 這樣的字串其實是整數串列
map#{name => "Ann"}鍵值儲存結構
binary<<"text">>位元組序列,是儲存文字的高效方式

true 與 false 是 atom,不是獨立的型別。= 是比對而不是賦值,所以第 3 行會失敗:Year 已經綁定為 1986。

模式比對、case 與 guard

模式比對可以拆解 tuple、串列與 map,並選擇要執行哪個子句。以 when 撰寫的 guard 可為子句加上條件。case 與 if 都是會回傳值的運算式。

-module(match).
-export([describe/1, grade/1, demo/0]).

describe({ok, Value}) -> io_lib:format("ok: ~p", [Value]);
describe({error, Reason}) -> io_lib:format("error: ~p", [Reason]);
describe(_Other) -> "unknown".

grade(Score) when Score >= 90 -> a;
grade(Score) when Score >= 70 -> b;
grade(_) -> c.

demo() ->
    [First | Rest] = [1, 2, 3],
    #{name := Name} = #{name => "Ann", age => 31},
    Size = case length(Rest) of
               0 -> empty;
               N when N < 5 -> small;
               _ -> large
           end,
    io:format("~p ~p ~s ~p ~p~n", [First, Rest, Name, Size, grade(75)]),
    io:format("~s~n", [describe({error, timeout})]).
% match:demo() prints:
% 1 [2,3] Ann small b
% error: timeout

除了最後一個子句外,每個子句都以 ; 結尾,最後一個以 . 結尾。如果沒有任何子句符合,行程會引發 function_clause 或 case_clause 錯誤。

函式、遞迴與高階函式

函式由名稱與元數識別,寫成 name/Arity。Erlang 沒有迴圈;重複的工作靠遞迴,而尾遞迴函式只使用固定的堆疊空間。匿名函式寫成 fun ... end。

-module(fns).
-export([factorial/1, sum/1, demo/0]).

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

sum(List) -> sum(List, 0).          % public wrapper
sum([], Acc) -> Acc;                 % tail-recursive helper sum/2
sum([H | T], Acc) -> sum(T, Acc + H).

demo() ->
    Double = fun(X) -> X * 2 end,
    io:format("~p~n", [factorial(20)]),                    % 2432902008176640000
    io:format("~p~n", [sum([1, 2, 3, 4])]),                % 10
    io:format("~p~n", [Double(21)]),                       % 42
    io:format("~p~n", [lists:map(Double, [1, 2, 3])]),     % [2,4,6]
    io:format("~p~n", [[X * X || X <- [1, 2, 3, 4, 5], X rem 2 =:= 1]]). % [1,9,25]

最後一行使用了串列生成式。請留意比較運算子:=:= 與 =/= 用來檢查完全相等與不相等,== 與 /= 則以數值比較(所以 1 == 1.0 為真),而小於等於寫成 =<,不是 <=。

串列、map 與標準函式庫

lists 模組提供 map、filter、foldl、sort、reverse 等許多函式;maps 處理 map;string 與 binary 處理文字。完整參考資料在 OTP 文件中。

1> L = [5, 3, 8, 1].
[5,3,8,1]
2> lists:sort(L).
[1,3,5,8]
3> lists:filter(fun(X) -> X > 2 end, L).
[5,3,8]
4> lists:foldl(fun(X, Acc) -> X + Acc end, 0, L).
17
5> M = #{ann => 31, bo => 25}, map_size(M).
2
6> M2 = M#{cy => 40}, map_size(M2).
3
7> maps:get(bo, M2).
25
8> maps:get(zed, M2, none).
none

M#{cy => 40} 會建立一個新的 map;M 本身絕不會改變。若要更新一個必須已存在的鍵,請用 := 取代 =>。

行程與訊息傳遞

行程是 Erlang 的招牌特色。spawn 啟動新行程,! 傳送訊息,receive 等待訊息,並以模式比對來配對訊息。行程成本很低,因此系統可以同時執行非常多個。透過 link 與 monitor,一個行程可以得知另一個行程已終止,OTP 的 supervisor 就是用這個機制自動重啟失敗的工作行程。

-module(counter).
-export([start/0, loop/1]).

start() ->
    Pid = spawn(counter, loop, [0]),
    Pid ! {add, 5},
    Pid ! {add, 10},
    Pid ! {get, self()},
    receive
        {count, N} -> io:format("count = ~p~n", [N])   % count = 15
    after 1000 ->
        io:format("timeout~n")
    end.

loop(Count) ->
    receive
        {add, N} -> loop(Count + N);
        {get, From} -> From ! {count, Count}, loop(Count)
    end.

這個手寫的迴圈,正是 OTP 的 gen_server behaviour 所泛化的做法。在正式環境的程式碼中,你會寫一個 gen_server 模組,並把它放在 supervisor 之下。

錯誤處理與「let it crash」

Erlang 有三類例外:error、throw 與 exit。可預期的失敗通常以 {ok, Value} 或 {error, Reason} 元組回傳。非預期的失敗往往完全不攔截:行程直接當掉,再由它的 supervisor 把它重啟到已知的良好狀態。真的需要攔截時,使用 try ... catch。

1> file:read_file("missing.txt").
{error,enoent}
2> try 1 / 0 catch error:badarith -> division_failed end.
division_failed
3> try throw(my_reason) catch throw:R -> {caught, R} end.
{caught,my_reason}
4> case file:read_file("missing.txt") of {ok, Bin} -> Bin; {error, Why} -> Why end.
enoent

完整的小程式:字詞頻率統計

這個模組用 map 統計字串中的字詞,依次數排序,並印出最常見的三個。它結合了字串函式、lists:foldl、map 與自訂排序。

-module(wordfreq).
-export([top/2, main/0]).

top(Text, N) ->
    Words = string:lexemes(string:lowercase(Text), " .,;:!?\n"),
    Counts = lists:foldl(fun(W, Acc) ->
                                 maps:update_with(W, fun(C) -> C + 1 end, 1, Acc)
                         end, #{}, Words),
    Sorted = lists:sort(fun({W1, C1}, {W2, C2}) ->
                                {-C1, W1} =< {-C2, W2}
                        end, maps:to_list(Counts)),
    lists:sublist(Sorted, N).

main() ->
    Text = "The cat and the dog. The dog sat, and the cat ran.",
    [io:format("~s: ~p~n", [W, C]) || {W, C} <- top(Text, 3)],
    ok.
% the: 4
% and: 2
% cat: 2

接下來可以學什麼

  • erlang.org 上的 Erlang/OTP 文件,包括 Getting Started 指南與 OTP Design Principles。
  • Learn You Some Erlang for Great Good!,一本可免費線上閱讀的書。
  • 標準建置工具 rebar3,以及與 Elixir 共用的套件管理工具 Hex。
  • 開始建構真正的服務後,請查閱 gen_server 與 supervisor 的參考頁面。

常見問題

Erlang 現在還有人用嗎?

有。Erlang 仍然運行著電信系統、訊息服務與 RabbitMQ,而 BEAM 生態系也透過可使用 Erlang 函式庫的 Elixir 與 Gleam 持續成長。

Erlang 的 OTP 是什麼?

OTP 是隨 Erlang 一起提供的一套函式庫、工具與設計原則。它提供 gen_server、supervisor 與 application 等 behaviour,用來組織容錯的程式。

該學 Erlang 還是 Elixir?

Elixir 的語法較親切,網頁生態系也更大;Erlang 則較精簡,OTP 與許多核心函式庫都是用它寫成的。兩者運行在同一個虛擬機上,懂其中一個,就很容易讀懂另一個。

let it crash 是什麼意思?

你不必在行程內處理每一種可能的錯誤,而是讓行程失敗,並依賴 supervisor 把它重啟到乾淨的狀態。錯誤處理的程式碼因此保持精簡,系統也能自動復原。

為什麼 Erlang 的變數要大寫?

Erlang 用大小寫來區分變數與 atom:Name 是變數,name 則是 atom,也就是常數值。

全部編碼教學