code

顯示具有 Erlang 標籤的文章。 顯示所有文章
顯示具有 Erlang 標籤的文章。 顯示所有文章

2017年3月13日 星期一

Erlang筆記14 - OTP 2: gen_server

使用OTP的gen_server步驟

(1) 決定你的callback module名稱
例如我們叫他my_bank

(2) 決定你的callback module的給client介面 
例如這個銀行的module可能有以下幾個functions:



這些interface會利用OTP gen_server裡面的function來implement:


可以看到start() function使用到了gen_server:start_link(),這是gen_server開啟一個server的方法,這邊開啟了一個local server,如果傳入global atom的話,就變成某個cluster都可以access的global server。

?MODULE是一個macro,會把目前的module name替換出來。

而gen_server:call()就是我們之前implement的rpc這種remote procedure call。


(3) implement callbacks
這邊就一定要符合OTP gen_server需要的 callback介面,否則應該會有runtime exception,包括以下六個 :

init/1 
handle_call/3
handle_cast/2 
handle_info/2 
terminate/2
code_change/3

記得要 export出去這六個 callback functions,否則gen_server module會無法呼叫它們。

某些editor(例如emacs)可能會提供skeleton template,例如這樣:


這邊有一個新的東西 -behavior(),意思是要跟compiler講幫我們檢查是否implement了所有gen_server要得callbacks,相當於java "implements" keyword。

這些callback中,最重要的是handle_call,而第一個參數實際上又有三種patterns:



以下是這個my_bank,erl的所有gen_server callback:


























2017年3月8日 星期三

Erlang筆記13 - OTP 1

什麼是Open Telecom Platform?

OTP是Ericsson為了大型分散式容錯系統寫的erlang library,這已經是standard erlang library。OTP包括完整的web browser, FTP server, .. 等等。

OTP特色就是Behavior framework。

OTP Behavior

OTP把常用的code pattern包成behavior application framework,只需要填入callback來handle event即可。其他所有fault tolerance, scalability, dynamic-code upgrade, 都不用擔心了,因為都被behavior framework implement好了,類似java j2ee container (我也不知道那是啥 哈哈)。

以下一步步示範怎麼從最簡單的server,長到OTP genserver裡面的server。

server1: 擁有callback的最可能簡單的server形式


這邊可以明顯看到我們在start function中接受一個Mod參數,此Mod參數必須implement
1. init(): 提供initial state
2. handle(): 就是真正的request handler

所以client先使用start來spawn一個process,然後用Name來bind這個server process的Pid。
然後再呼叫rpc來對sever下Request。注意rpc function也有 receive/end clause,這是對caller process來說安裝一個message receiver。

所以當Mod在loop中根據目前的State handle了Request之後,loop會遞迴呼叫自己,這時候Mod:handle()處裡的則是新的State1,所以 Mod本身不會有State,符合immutable的本性。

mod1

接下來該寫mod1。
可以看到server1.erl 其實就是個骨架而已,Mod1.erl才是真正的server implementation。



可以看到mod1的確implement了必要的 callbacks。所以這個server initial state是一個空的 dictionary,另外可以handle add和find兩種Request,並且接受新的Dict當作新的state,並且回傳改變過的Dict,相信dict:store()應該也是immutable implementation,意即回傳一個copy。

client實際上能看到的應該只有add和find function,handle function理論上client不該使用。

重點是:callback的implementation完全不用管concurrency model!!!!! 這很強大。

client1

在client process:

啟動service process:
2> server1:start(mod1, mod1).
true

這邊把Pid寫成mod1,其實是寫死的,因為我們在mod1.erl裡面也寫死Pid為mod1。


使用add
3> mod1:add("Sonny", "Office").

ok

使用find
4> mod1:find("Sonny").
{ok,"Office"}


就這樣,不過這樣其實蠻不直覺的interface!!!!


server2: transaction thru error handling

server1進化成serever2,藉由catch exception來確保"transaction semantics"執行。




可以看到在loop function裡面,如果執行了callback handle function失敗的話,loop會recursively calls itself with "OldState",所以下一個message來要求執行handle()仍然是使用OldState,也就是transaction失敗的話不會改變狀態。

但是如果沒有catch 任何exception 的話,loop就會recursively calls itself with NewState。


rpc function裡面也幫client新增了一個對crash atom的handler,直接exit,所以其實有改變了client 的behavior! 


Server 3: A Server with Hot Code Swapping

另一個可以神奇做到的是server code熱切換,意即不用把server給停下來。
只要改動loop:


可以看到當loop match到swap_code這個atom的時候,先送一個ack給message sender,然後loop很簡單的就靠recursion重啟了,並且使用了新的callback function,而且state不會改變!

這就是stateless的好處!!! 這在imperative server應該非常困難做到。


Server 4: A Becoming Server

這有點新時代的意味了?!

既然可以dynamic swap module,那甚至可以dynamic swap loop function!
一個本來在idle無所事事的server:


可以切換成以下的service:



只要送一個message給此process即可:



OK 以上別當真!

意思是上面的code / design 不是太好,只是作者要demonstrate distributed erlang的能力。總之直接使用OTP framework就可以了。


2017年3月3日 星期五

Erlang筆記12 - Distributed Programming 2

Remote Spawning

我們如何在遠端的node spawn process?
可以利用erlang spawn primitive的其中一個remote 版本:

-spec spawn(Node, Mod, Func, ArgList) -> Pid

以下是一個範例,我們希望達成在local node可以對遠端node Node spawn一個process,然後跑我們指定的module M的function F,以及其arguments list A

首先我們需要一個讓local node可以指定在遠端Node生出process的介面:

-module(remote_spawn).-export([start/1]).
start(Node) ->
  spawn(Node, fun() -> loop() end).

注意local node必須在呼叫remote_spawn:start()之後,用一個Pid去承接return回來的process id。

我們在start裡面,對遠端Node做spawn process的動作,執行loop這個function。
再來定義loop:

loop() ->
  receive    {rpc, Pid, M, F, A} ->
      Pid ! {self(), (catch apply(M, F, A))},
      loop()
  end.

loop會match {rpc, Pid, M, F, A} ,然後對此Pid發送spawn需要的參數訊息。

再來就是要包裹loop,加上sender的Pid,建立一個rpc function讓local node可以使用:

rpc(Pid, M, F, A) ->
  Pid ! {rpc, self(), M, F, A},
  receive    {Pid, Response} ->
      Response  end.
怎麼使用?我們先在同一台電腦上測試,建立兩個不同的erlang node:


Rita-Tsai:src sonnywang$ erl -sname gandalf -setcookie abc
Rita-Tsai:src sonnywang$ erl -sname bilbo -setcookie abc


然後在bilbo建立gandalf的process:
(bilbo@Rita-Tsai)6> Pid = remote_spawn:start('gandalf@Rita-Tsai').
<9993.80.0>

這個在gandalf的process建立了,而且成功回傳了Pid = <9993.80.0>
bilbo 可以對此remote process要求執行某個function,例如:

(bilbo@Rita-Tsai)8> remote_spawn:rpc(Pid, erlang, node, []).
'gandalf@Rita-Tsai'


gandalf回傳了gandalf node。(node()是回傳自己是哪一個node, nodes()回傳還有其他哪些nodes)。

或是要求print某個字串:

(bilbo@Rita-Tsai)12> remote_spawn:rpc(Pid, io, format, ["qq~n"]).
qq
ok


放在AWS兩個不同instance建立的nodes應該也會有同樣的效果。

2017年3月2日 星期四

Erlang筆記10 - Publish PID

Publishing PID

一個spawn出來的PID只有spawn它的人才會看到,這一方面很安全(不相干的人看不到),但是另一方面會有無法跟其他processes溝通的問題,所以我們需要有方法來publish一個PID給有需要的processes看到的方法,稱為registration。而一個published PID則稱為registered process。

其實就是bind一個假名atom啦!

4個BIFs可以做到此點:


例如以下的clock process,我們在start function中bind spawn出來的pid到atom clock,可是有個疑問就是,這樣除非這個process terminated,否則每次call start()應該都會register失敗,因為clock atom已經被用了。



Erlang筆記10 - process spawn需要的時間

測試一下spawn一個process需要的時間

先來看以下這段code:

-module(processes).-export([max/1]).

max(N) ->
  Max = erlang:system_info(process_limit),
  io:format("Maximum allowed processes:~p~n",[Max]),
  statistics(runtime),
  statistics(wall_clock),
  L = for(1, N, fun() -> spawn(fun() -> wait() end) end),
  {_, Time1} = statistics(runtime),
  {_, Time2} = statistics(wall_clock),
  lists:foreach(fun(Pid) -> Pid ! die end, L),
  U1 = Time1 * 1000 / N,
  U2 = Time2 * 1000 / N,
  io:format("Process spawn time=~p (~p) microseconds~n",
    [U1, U2]).wait() ->
  receive    die -> void
  end.
for(N, N, F) -> [F()];
for(I, N, F) -> [F()|for(I+1, N, F)].

黃色部分是一個for function的定義,這個把N個F()的結果給放到一個 list中。
所以可以看到L是一個N個 spawned processes PIDs的list,每一個process只是單純的執行wait()。
wait to die!!!!!

然後在綠色部分做一個map的動作,這個mapping其實就是把process kill掉而已。
再來就是把這N個process spawn用system time來算出執行時間。

在我的電腦上,結果如下:
Maximum allowed processes:262144
spawnedProcess spawn time=3.1 (1.55) microseconds


增加max allowed processes

262144 = 2^18
我們試試看加大erlang emulator能乘載的processes數目:


其實就看你的電腦記憶體有多大,我開500萬個processes,因為記憶體只有16g,所以需要至少 > 10g,所以還沒開到500萬個就爆掉了。


timeout

這可以拿來寫成process waiting timeout:


或是timer:
(沒有receive 會compile不過! 所以是為什麼?)

或是做buffer flushing:


這段code的意義是 由於after 0,所以馬上就timeout return true,但是在 return 之前,會先執行一次receive clause,所以match anything之後又recursively call itself,這樣就會一直把received到的東西給讀取(但是沒做任何事),直到再也沒有_Any可以match就結束了這個process。

如果拿掉after 0 clause的話,這個process要是沒有收到東西也不會結束自己。

利用timeout時候 (i.e. after 0)會先檢查receive clause的語言特性,可以做出很多變化,非常好用!

在語言層級支援這個還蠻神奇的。



一個可以 cancel的timer

-module(mtimer).
-export([timer/2, start/2, cancel/1]).
timer(Microsecs, Func) ->
  receive    cancel ->
      void
  after Microsecs ->
    Func()
  end.
start(Microsecs, Func) ->
  spawn(fun() -> timer(Microsecs, Func) end).
cancel(Pid) ->
  Pid ! cancel.


這樣的架構就用到了after 的好處。使用如下:



2017年3月1日 星期三

Erlang筆記9 - 簡單client-server架構 in Erlang

Erlang processes DONT share memories

erlang processes是小型的erlang VM。這是erlang語言層級的process,不是OS層級的,所以在任何erlang VM上都能保持一致行為。

兩個erlang processes之間不share memory,這很合理,連單一erlang process之內都不能change memory data了,何況是share給另一個erlang process!

這讓scalability變得很容易,因為彼此processes是獨立的,所以增加workload就可以簡單地增加人力(processes)來因應。

不分享記憶,就不用保護記憶,就沒有lock & keys。
沒有lock & keys,就不會有一大堆相關的錯誤,特別容易出現在concurrent programming。

Error Detection

既然processes沒有share memory,怎麼辦法溝通?
靠send messages。(erlang是一個pure message-passing language),不過messages有沒有被其他人收到或是處裡的話,還要再send messages去詢問對方 (除非對方send ACK回來)。

另外一個process死掉會broadcast出來給其他processes知道,某個跟他“link關聯"的process會接手 (稱為linked pair)。這樣就能做error detection。


Concurrent Primitives

Erlang concurrent program只需要以下三個primitive operations就可以構成一個concurrent program:

1. spawn: 產生一個erlang process。如果要refer到這個產生的process的話,就靠此 operation回傳的Pid。

另一個版本為:


2. send message: 傳送message 給某一個Pid。比較特別的是 ! 是send operator


此operator "似乎" (還待證實)會回傳送出去的message,所以我們可以chaining 這個operators:

Pid1 ! Pid2 ! … ! Msg ,代表對Pid1 Pid2 ... 送出Msg 這個messageㄡ



3. receive:
如果想要等待某個message,就可以用此operation配合上pattern-matching:
receive的語法如下:


Process Example

先寫一個process專門計算面積的:

-module(area_server0).-export([loop/0]).

loop() ->
  receive    {rectangle, Width, Ht} ->
      io:format("Area of rectangle is ~p~n",[Width * Ht]),
      loop();
    {square, Side} ->
      io:format("Area of square is ~p~n", [Side * Side]),
      loop()
  end.

這個server process簡單來說就是等待message,如果match {rectangle,W,H} 或是 {square, S} pattern的話,就印出面積,然後等待下一次的message進來。

我們可以在Erlang console spawn此server process以及其receiver:

6> Pid = spawn(area_server0, loop, []).
<0.64.0>

有了Pid之後,用 ! operator來傳送符合pattern的message給Pid:

9> Pid ! {square, 10}.
Area of square is 100
{square,10}


這樣就完成了一次簡單的messaging, 可以看到console另外也印出了message,這表示 ! operator的確是回傳message (書上是說 Pid ! Msg被定義成Msg),有待查證到底哪一個對。


Client-Server Example

真的要變成一個client-server架構的話,至少還缺了client的Pid,因為沒有這個server等於沒辦法將計算結果回傳給client。

改寫server如下:

-module(area_server0).-export([loop/0]).

loop() ->
  receive    {FromPid, {rectangle, Width, Ht}} ->
      FromPid ! Width * Ht,
      loop();
    {FromPid, {square, Side}} ->
      FromPid ! Side * Side,
      loop()
  end.

可以看到pattern matching改了,而且server process也回送message 給FromPid,所以我們的client也必須要是一個Erlang process才能收到server的message,我們先把server延伸成有client能力(能夠發request也能等待回應):

rpc(ToPid, Request) ->
  ToPid ! {self(), Request},
  receive {Response} ->
    Response  end.

如果發以下指令:
1> Pid = spawn(server, loop, []).
<0.57.0>
2> server:rpc(Pid, {square, 10}).

是可以回傳面積的,可是在IntelliJ Erlang plugin的console不知道為什麼出不來,不過在一班的erlang console是可以的。


client如何只回應特定的server?

pattern matching這邊發揮美麗的特性:

rpc(ToPid, Request) ->
  ToPid ! {self(), Request},
  receive    {ToPid, Response} ->
    io:format("Area of rectangle is from ~p, response = ~p ~n",[ToPid, Response]),
    Response  end.

loop() ->
  receive    {FromPid, {rectangle, Width, Ht}} ->
    FromPid ! {self(), Width * Ht},
    loop();

    {FromPid, {square, Side}} ->
      FromPid ! {self(), Side * Side},
      loop()
  end.


2017年1月22日 星期日

Erlang筆記8 - Maps

Instantiation

廢話不多說,看code就能懂:



不過可以看到,print出來的map都是ordered by key,因為其內部使用了ordered list來implement。


Update key-value pair

有兩個operators,語意不同。



=> operator語意是 update or create
:= operator語意是update or fail

要看application的意圖是什麼,來選擇適當的operator。


Pattern Matching

發現書上寫的已經有點outdated了。
之後再來補充這邊。



Erlang筆記7 - Records

Records


records和tuple同一掛的,沒什麼特別的。
records, tuple都是不能動態改變collection length的(不能新增或刪除item),而list, map可以,差別就在這。

通式如下:

宣告record definition,但是還沒實體化!


records有點像是ios plist,是可以存在一個.hrl file,這樣可以對不同的module都visible。
實體化的時候,如果跳過一個key沒給值的話,則erlang會給予一個definition中的default value。

實體化,語法上很怪,用#號表示instantiation:


第四行是指make a copy of X1 and update "status" field。

extraction:


或是用dot syntax:


總之是令人不習慣的語法,藍瘦香菇~~


pattern matching:

先貼上來,有用到再說。我發現我幾乎沒用過類似record的時候。



Erlang筆記6 - Appending a List, 以及Accumulator的使用

忠告:Appending a List

List在appending時,最好放在head,例如[H|List]來形成一個新的list。
之後如果有需要再call lists:reverse(L),這是erlang compiler 高度最佳化的function。

避免使用 List ++ [Head] 來做list concatenation。因為這是極度沒效率。
不過除非performance是個issue,否則上述的code有邏輯清楚的優勢,就看你的取捨。


Accumulator

有時候list comprehension不夠有效率,例如以下的程式需要traverse list兩次才能產生兩個偶數和奇數的lists:


所以這時候就要自己implement 這個function,我們可以用accumulator的方式來寫,這是標準的FP寫法之一(或許甚至不需要accumulators,只是在這邊紀錄Erlang的accumulators寫法):





Erlang筆記5 - Guards, case, if

其實erlang的function如果完全按照寫成pattern matching clauses,那對初學者是很不容易的,因為這根本就是Prolog那種邏輯!

(不過我覺得很好玩就是)

所以類似Scala,Erlang提供了其他的control construct讓人們的日子好過一點。


Guards

這是pattern matching的延伸,加入一些boolean expression幫助matching,使用的是when這個keyword:


第一個clause要matching成功必須也要 X > Y evaluated to true。

另一個例子:

這邊有一個is_integer function,這是built-in function,因為是一個boolean function,所以也可以當作一個guard。上面所有的逗號隔開的 boolean expressions都必須要“同時成立”,因為逗號相當於boolean AND。



case

再來就是提供跟Scala很像的case,有case基本上就不用寫的Prolog一樣了,可以寫得跟Scala完全一樣:



if

這個基本上就完全跟pattern matching沒關係:


注意如果沒有任何Guard被evaluated to true,會有runtime error。所以通常最後一個會放atom true -> expression。


Erlang筆記4- List Comprehensions

list comprehension

一個更簡潔建立list的方法就是list comprehensions,舉例來說,代替list map如下:


意思是現有的list L中,每個X要被F map成新的value,然後形成一個新的list。
當然X要match L中每個element的pattern。


通式如下:

Qualifier包括:
generators,從某個collection中抽出其中的element pattern,語法為 X <- List
filters,boolean predicates,例如 X > 1

這跟Scala的for generators一樣,所以還是有共通之處。


範例1: quicksort


++ [Pivot] ++ 是infix appending,先不用管他。
可以看到有generator 和 filter qualifiers 組成的list comprehensions。


範例2: 畢氏定理


這個範例中的qualifier是generators,總之generator要能被evaluated to a collection (erlang應該不止有list吧? 待後觀來),所以lists:seq(1,N)產生[1,2, ... , N],是一個合法的generator。

注意 erlang的 boolean operator跟一般語言不太一樣:

X =< Y 解讀成 X小於等於Y的predicate
X =:= Y 解讀成 X等於Y的predicate


範例3: Anagrams


這個有點神奇,竟然兩行就寫完了anagrams演算法?!來看跑起來是什麼結果:


其實沒有啦,愛唬爛!
anagrams的定義不是這樣的,這應該叫做list的所有permutation。

第二個clause的recursion我們先不理他了,這邊只是要告訴你list comprehensions的qualifiers要怎麼寫。這裡的重點是可以是recursion,只要type 能match到collection或是list就可以。


2017年1月21日 星期六

Erlang筆記3 - Functions

module

erlang的module是file scope,所以某個module "geometry"其實就是一個同名的geometry.erl檔案。在此檔案中,可以決定要export什麼functions給其他的module看:


上面area其實是同一個function,但是因為可以接受不同的參數pattern(或說是function head pattern),所以形成了兩個不同head->body pair的clauses。兩者參數的pattern都是arity-1,雖然tuple的長度不一樣。

function的pattern matching採取了類似prolog的clause語法,而不是像scala那樣是一個無關的function name。另外function name是一個atom (或說symbol or constant),所以必須要小寫。

注意pattern matching的順序跟clause宣告順序一至,如果沒有pattern matched,會產生runtime error!


宣告functions 

上面已經可以看到一個function長什麼樣子。另外return value跟其他FP一樣,就是function中最後一個被evaluated expression。

呼叫functions必須加上module name,當然parameters patter也必須match:


如果要寫一個多行的function,則每行用 , 隔開:



lambda / anonymous function

在erlang中,一個function object被稱為 “fun”。

其實funs 比較像是一般程式語言宣告一個function時候的語法和概念,不過他必須用一個variable去bind:



當然可以接受任意參數:

不過由於funs必須要用variable去綁定,但是variables是不能export出去module的,所以註定了funs只能當成某個function的參數或是local anonymous function,甚至不能單獨在一個module中宣告(會compile error),而是必須在某個function中被宣告才行。


first-order functions

lists:map()是一個first order function,因為它能接受funs當作參數:



其他大概都跟Scala或是一般的FP差不多。