我的雲端生活網 - Life+

Tuesday, May 12, 2009

Erlang gen_server 行為解析

本文為翻譯 gen_server behaviour in Erlang (http://geeklair.net/~pratzsch/blog/2008/04/gen-server-behaviour-in-erlang.html)一篇.

授權聲明:本篇是由 Phil Ratzsch 發表於個人 blog 「你不愛讀的東西」(You dont't want to read this. http://geeklair.net/~pratzsch/blog/) ,文章發表於 2008 年 4 月 30 日晚間。我已透過電子郵件徵求取得作者許可,在此張貼翻譯版本。非常感謝 Philip Ratzsch 閣下。
Claim: This article by Phil Raztsch is posted in his blog "You dont' want to read this" in the evening at April 30, 2008. I've got the author's permission that allows me to post my translated version. I would like to convey thanks to Mr. Philip Ratzsch.

標色文字是我的註解。


Erlang gen_server 行為解析

本文由 Phil Ratzsch 於 2008 年 4 月 30 日晚間發表

你們有些自己在家玩些什麼 (我說:宅男嗎?) 的人都知道我有點喜歡 Erlang 全部的東西。對啊!你們一些在家玩自己的 (還有任性的衝浪人,也是) 、與歐洲的戶外隔離的人,可能想知道為什麼 behaviour 字裡躲著一個煩人的 u 字母。 Erlang 來自 Ericsson 公司 (他們公開否認 Erlang 和 Ericsson Language 一詞的關係,而說 Erlang 跟著一位同名數學家的名字定名) 而且 Ericsson 正好位於 Scandanavia 地區。 (據查:Scandanavia 是北歐包含丹麥、瑞典和挪威三國的區域)

語言家的理論說可以隨他們喜好、用 u 代替「 o 中間劃一線」字母。 (我說:那是一個丹麥文字母 Ф ,做母音有時音同 bird 或 hurt 、有時音同 sister 的 i 音。) 或是可能他們想表現出多麼愛那個空集合符號。不管這個,我們跳進 gen_server 話題吧。

gen_server 是 OTP ──「公開電傳平台」(Open Telecom Platform) ── 的一項元件。 OTP 可說是 Erlang 的一種應用程式架構。像 Python (蟒蛇) 有 Pylons (高壓電線架) ,以及 Ruby (寶石) 有 Rails (軌道或扶手) ,還有 Windows 有「當機的毛病」。 OTP 有強大威力,令人喜不自禁;而身為相當新手的我,只開始領會它有多強。引述 Joe Armstrong 寫的 "Programming Erlang" 書中字句:
OTP 的能力來自於一些特性,諸如容錯、可延伸能力、以及動態碼的更新等等,都由它的行為提供。也就是說,寫 callback 的程式人不用煩這些事情了,因為 OTP 行為會提供這些能力。
好的,菲利普,「行為」是指什麼呢?一件行為,就是像它聽起來像是的東西 ── 像一個桶子裝了一種系統或平台的全部普通行為。 (我說:這到底是什麼樣的直覺?)

舉一個很傳統的人為範例,假想一家錄影帶店只讓你一次買一部片。程式碼可能看起來像下列我收錄的。像上次一樣,先來個程式,再來個說明。我無限地感謝執行 CouchDB 專案的主持人 Jan Lehnardt 的幫助、耐心指導和友誼。將來不久,他也會貼一筆同樣主題的 plok 記錄,所以請務必去看一下,而且要給他錢。 (我說: plok 是很像 blog 的東西。)

-module(movie_store).
-behaviour(gen_server).

-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-export([checkout/2, lookup/1, start_link/0]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
checkout(Customer, Movie) -> gen_server:call(?MODULE, {checkout, Customer, Movie}).
lookup(Customer) -> gen_server:call(?MODULE, {lookup, Customer}).

init([]) ->
Tab = ets:new(?MODULE, []),
{ok, Tab}.

handle_call({checkout, Customer, Movie}, _From, Tab) ->
Response = case ets:lookup(Tab, Customer) of
[] ->
ets:insert(Tab, {Customer, Movie}),
{ok, Movie};
[{Customer, OtherMovie}] ->
{already_checked_out, OtherMovie}
end,
{reply, Response, Tab};

handle_call({lookup, Customer}, _From, Tab) ->
Reply = case ets:lookup(Tab, Customer) of
[{Customer, Movie}] ->
Movie;
[] ->
none
end,
{reply, Reply, Tab}.

handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Msg, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVersion, State, _Extra) -> {ok, State}.

好,看起來很龐大 (順便地說,也是 Erlang 的超級威力) 但其實不是。第一行宣告了模組名稱 (而且 movie_store.erl 必須是檔名),這是 Erlang 每個程式都有的標準格式。

-behaviour(gen_server). 可想成是宣告我們程式裡用什麼「模版」。 gen_server 的行為要求我們要定義列在第一組匯出 (exports) 的函數。

-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 是 gen_server 匯出並尋找的函數 (,而且如果缺了他們,可會抱怨得很大聲喔) 。下篇文章我會探究它們、深入到細節 (我說:但他的下一篇文章,我不見得會譯完。) ,恰如這會是一篇長篇大作。現在,要曉得,就算這些函數都不做事情,也要定義這些函數全部。如你所見,尾端一些程式像 handle_cast/2 、 handle_info/2 、 terminate/2 、 code_change/3 ,都不做事情。

-export([checkout/2, lookup/1, start_link/0]). 製做自訂的函數,讓 server 可使用它們於外界。這些函數列在另一個 -export 敘述中,以資區別,或者也可以與前面的 -export 合併。

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

程式的起點。如你所見,這個函數是 server 運作命令的包裝。{local, ?MODULE} 顯示 server 會以區域方式執行,並不會讓叢集系統的其他 Erlang 節點存取。 ?MODULE 是現有程式的替身,指示我們該用什麼東西參考這個 server 。第二個 ?MODULE 告訴 server 往哪裡找到 callback (在此例是同一個檔案) 。其他選項容許開啟除錯功能、做檔案記錄、以及其他未列入此例的項目。

checkout(Customer, Movie) -> gen_server:call(?MODULE, {checkout, Customer, Movie}).
lookup(Customer) -> gen_server:call(?MODULE, {lookup, Customer}).

這幾行聯繫自訂函數,告訴 gen_server 當 movie_store:checkout(...) 和 movie_store:lookup(...) 函數呼叫時能做什麼。 ?MODULE 指定函數所在的檔案和模組,而 {some_name, Arg1, Arg2, ...} 提供函數名稱和傳入的參數。
init([]) ->
Tab = ets:new(?MODULE, []),
{ok, Tab}.
用 ets 建立資料表並傳回。 ets 是內建的記憶體資料庫。在部署的程式中,這或許有時有些 ... 耐久。 gen_server 的關鍵元件是程式狀態。在本例中,你將看到 Tab 被丟來丟去,丟到每一樣程式單元中,表達現有的錄影帶店狀態 (誰購買了什麼片) 。
handle_call({checkout, Customer, Movie}, _From, Tab) ->
Response = case ets:lookup(Tab, Customer) of
[] ->
ets:insert(Tab, {Customer, Movie}),
{ok, Movie};
[{Customer, OtherMovie}] ->
{already_checked_out, OtherMovie}
end,
{reply, Response, Tab};
記得之前說的一點 gen_server:call(...) 嗎?這是 gen_server 呼叫的東西。你們可能想知道為什麼 handle_call 定義了二個函數,而且引用相同數量的參數 (參數數量表達為 /x ,如 "functioname/2" 裡面的 /2 ) 。看它第一個參數,是有三項的一列記錄。再看看

checkout(Customer, Movie) -> gen_server:call(?MODULE, {checkout, Customer, Movie}).

第一個項目符合樣式 (我說:指 {checkout, Customer, Movie} ) ,這是 gen_server 知道如何處理 handle_call 呼叫 ... 那個 ... 呼叫。嗯,最後一截用太多「處理」和「呼叫」的字眼了。重點是就像其他許多 Erlang 的東西一樣,以符合樣式為基礎。

注意一下, Tab 表格也傳入了。我們就進到一個 case/of 段落,檢查客戶在表格中有沒有購買了影片。如果沒找到記錄 ([] -> ...) ,片子就賣給他們,在此段落傳回 {ok, 影片名稱} 。如果已經有記錄,則傳回 {already_checked_out, 另一影片名稱} 並拒絕客人購買片子。 (注意,本例沒有讓客人退貨的辦法。這個留給讀者你自己練習。)

本段落任何一個情況都將傳回值塞進一列記錄,發射回給呼叫這個函數的人。在往下一個 handle_call(...) 走去之前,看一下最後一行: {reply, Response, Tab}; 。你可能期待有個句點而不是分號。 handle_call(...) 句子是用分號結尾,而且如果是改用像其他函數一樣的結尾。我好不容易才找到它的道理。 (我說:分號表示同一函數的兩套不同的定義之間的分界。)
handle_call({lookup, Customer}, _From, Tab) ->
Reply = case ets:lookup(Tab, Customer) of
[{Customer, Movie}] ->
Movie;
[] ->
none
end,
{reply, Reply, Tab}.
這個 handle_call/3 定義一個函數檢查看看客人有沒有購買了影片。如果有,傳回一筆記錄包含了客人和電影的名字。如果沒有,則傳回 none 。 (我說:重講一次,應該說是找得到一筆記錄符合 {Customer, Movie} 就傳回電影名稱,否則,如果找不到就傳回 none 。) 我不要講這個太多,因為以正在講的 handle_call/3 來說,這個東西太細。

handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Msg, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVersion, State, _Extra) -> {ok, State}.

這段程式要擺在那邊,使 gen_server 能動;但這次我們沒用到它們。下一篇導引文章我會多談談。但我簡短說明: code_change/3 超酷,它讓 server 還在服務的時候可以將程式內容做熱抽換。要知道,想要改換程式何必麻煩讓 server 停掉呢,是吧?

最後,這是如何執行的示範:
2> c(movie_store).
{ok,movie_store}
3> movie_store:start_link().
{ok,<0.42.0>}
4> movie_store:checkout(phil, "Sneakers").
{ok,"Sneakers"}
5> movie_store:lookup(phil).
"Sneakers"
6> movie_store:checkout(phil, "Koyaanisqatsi").
{already_checked_out, "Sneakers"}
7> movie_store:lookup(phil).
"Sneakers"

(我說: 2 號命令是編譯 move_store.erl 。 3 號命令呼叫 movie_store:start_link(...) ,它轉呼叫 gen_server:start_link(...) 得到 {ok, <0.42.0>} ,表示命令完成,<0.42.0> 是 server 的程序代號。 Koyaanisqatsi (印地安語,意思是 life out of balance ,失衡的人生) 是一部藝術片,參考 IMDB 條目 (http://www.imdb.com/title/tt0085809/) 和 Wikipedia 條目 (http://en.wikipedia.org/wiki/Koyaanisqatsi) 可知道更多資訊。)

有個問題是程式在你試著選購 Koyaanisqatsi 時不會對你恭維幾句。嗯,我已經太煩人一整天了,至少希望你感興趣。如果你們要改就全都改,或者無論如何務必讓我知道。



建言:

Ulf Wiger 在 2008 年 5 月 1 日上午,說:

丹麥人和挪威人才用 o 中間砍一線,而瑞典人沒有這樣做。咱們都用 Ö 和 Ä ,很像德國人用的,但又多了很美的字母 Å 。對丹麥數學家致敬不代表分擔了使用 Ø 的原罪。 Erlang /OTP 團隊充斥著英國人,英文 behaviour 的拼字法可能源於那邊 ...

但不全沒有。若要建立自訂的行為, linter 接受 -behaviour() 和 -behavior() 兩種,還有 -behaviour_info() 和 -behavior_info() 。

還有,我聽說 Joe Armstrong (英格蘭人) 、 Mike Willams (威爾斯人) 、 Robert Virding 以前住在澳大利亞。

Harishi Mallipeddi 在 2008 年 5 月 2 日近午,說:

OTP Design Principles 一篇提供很有用 (至少對我來說) 的學習方法,學會用 OTP 的東西,包括 gen_server 、 gen_fsm 、其他等等。 Joe 的書可沒好好解釋 OTP 。

http://www.erlang.org/doc/design_principles/part_frame.html

No comments:

Blog Archive