2014/2/26

erlang basics - binary, bitstring, BIF

BIF: Built-In Function

BIF 是 erlang 內建的函數,例如 tuple_to_list/1 可將 tuple 轉換為 list,time/0 可取得當前的時間,大部分的 BIF 都屬於 erlang 模組,但因為 BIF 是自動匯入的,所以使用 tuple_to_list(...) 不需要寫成 erlang:tuple_to_list(...)。

http://www.erlang.org/doc/man/erlang.html 可找到 module:erlang 的文件。

函數規格 @spec

在 function 中,參數與回傳值的資料型別並不是清楚地寫在 function 的定義上,我們需要一個方式,來告訴使用這個函數的progarmmer,該怎麼使用它。erlang 社群開發了一種記號法,但這個記號法並不是 erlang 程式碼的一部分,而只是一種寫文件的工具。

這個記號法只能用在文件上,在程式碼中,會用 %% 將該行視為註解。通常會這樣寫 %% @spec

-module(math)
-export([fac/1]).

%% @spec fac(int()) -> int().

fac(0) -> 1;
fac(N) -> N * fac(N-1).

在使用此型別記號法時,要定義兩件事:型別與函數的規格。

定義型別

名稱為 typeName 的型別會寫成 typeName()

預先定義的型別是:

  1. any(): 指任何 erlang 的資料型別,term() 是 any() 的別名
  2. atom(), binary(), float(), function(), integer(), pid(), port(), reference(): erlang 的基本資料型別
  3. bool(): atom(true 或 false)
  4. char(): integer() 的子集合,代表字元
  5. iolist(): 遞迴地定義為 [char() | binary() | iolist()],通常用來產生高效率的字元輸出
  6. tuple()
  7. list(L): 是 [L] 的別名
  8. nil(): 就是 []
  9. string(): list(char()) 的別名
  10. depp_string(): 遞迴地定義為 [char()|deep_string()]
  11. none(): 沒有資料型別,用在不會產生回傳值的函數,例如無窮的接收迴圈,表示此函數不會返回

使用者自己定義型別可以寫成
@type newType() = TypeExpression

範例
@type onOff() = on|off.
@type person() = {person, name(), age()}.
@type people() = [person()].
@type name() = {firstname, string()}.
@type age() = integer().

指定函數的輸入與輸出型別

寫法為
@spec fuinctionName(T1, T2, ..., Tn) -> Tret
T1, T2, ..., Tn 是 參數的型別, Tret 是回傳值的資料型別

每個 T 都有三種可能的形式

  1. TypeVar
    型別變數,這代表未知型別(跟 erlang 的變數無關)
  2. TypeVar::Type
    型別變數後面跟著一個型別
  3. Type
    型別表示式

範例

@spec file:open(FileName, Mode) -> {ok, Handle} | {error, Why}.
@spec file:read_line(Handle) -> {ok, Line} | eof.

file:open/2 意思是,要開啟 FileName,會取得回傳值 {ok, Handle} 或是 {error, Why}
FileName 跟 Mode 是型別變數,但我們不知道它確切的型別是什麼。

範例

@spec lists:map(fun(A)->B, [A]) -> [B].
@spec lists:filter(fun(X) -> bool(), [X]) -> [X].

範例

@spec file:open(FileName::string(), [mode()]) -> {ok, Handle::file_handle()} | {error, Why::string()}.
@type mode() = read|write|compressed|raw|binary| ...

範例

@spec file:open(string(), Modes) -> {ok, Handle} | {error, string()}
    Handle() = file_handle(),
    Modes = [Mode],
    Mode = read|write|compressed|raw|binary| ...

範例

@spec file:open(string(), [mode()]) -> {ok,file_handle()} | error().
@type error() = {error, string()}.
@type mode() = read|write|compressed|raw|binary| ...

在文件中的定義

在文件裡,我們會省略 @spec

file:open(FileName, Mode) -> {ok, Handle} | {error, Why}.
    根據 Mode 開啟檔案 FileName。Mode 為....
file:read_line(Handle) -> {ok, Line} | eof.
    從開啟的檔案 Handle 中讀取一行資料,傳出 Line,檔案結尾則傳出 eof

使用到 @spec 的工具

EDoc
erlang 的文件產生器,類似 javadoc,以 @name, @doc, @type, @author等annotation,將文件嵌入到 source code 的註解中

Dialyzer
是靜態分析工具,可找出程式的型別錯誤、無法執行到的程式碼、無必要的測試...

binary

binary 可儲存大量的原始資料,以 << 與 >> 將一串整數或字串包夾在中間,整數必須要在 0 ~ 255 中間,<<"cat">> 是 <<99,97,116>> 的速寫,如果 binary 裡面都是可列印的字元,shell 就會自動當作字串列印出來。

1> <<5,10,20>>.
<<5,10,20>>
2> <<"cat">>.
<<"cat">>
3> <<99,97,116>>.
<<"cat">>

處理 binary 的 BIF

@spec list_to_binary(IoList) -> binary()
list_to_binary 以 IoList 內的整數與二元,產生 binary,IoList 是一個 list,裡面的元素是 0~255 整數、binary或 IoList。

4> Bin1 = <<1,2,3>>.
<<1,2,3>>
5> Bin2 = <<4,5>>.
<<4,5>>
6> Bin3 = <<6>>.
<<6>>
7> list_to_binary([Bin1, 1, [2,3,Bin2], 4|Bin3]).
<<1,2,3,1,2,3,4,5,4,6>>

@spec split_binary(Bin, Pos) -> {Bin1, Bin2}
在 Pos 位置,將 Bin 分割為兩個部份

8> split_binary(<<1,2,3,4,5,6,7,8,9,10>>, 3).
{<<1,2,3>>,<<4,5,6,7,8,9,10>>}

@spec term_to_binary(Term) -> Bin
將任意的 erlang term 轉換為 binary,將 term 轉成 binary 之後,就可以儲存到檔案、傳送到網路上,而且可以重建出原始的 term。

@spec binary_to_term(Bin) -> Term
term_to_binary的相反,可將 Bin 轉換為 Term

9> B = term_to_binary({binaries, "are", useful}).
<<131,104,3,100,0,8,98,105,110,97,114,105,101,115,107,0,3,
  97,114,101,100,0,6,117,115,101,102,117,108>>
10> binary_to_term(B).
{binaries,"are",useful}

@spec size(Bin) -> Int
這會傳出記憶體中的位元組個數

11> size(B).
29
12> size(<<1,2,3,4>>).
4

位元語法

這是 pattern matching 的擴充,用來取出並打包位元資料中的個別位元或位元序列。這個功能很適合用來撰寫低階程式碼,或是網路通訊程式,這是 erlang 最強的功能。

在變數 M 中,X 佔用 3 個位元,Y 佔用 7 個位元,Z 佔用 6 個位元。

14> X=2.
2
15> Y=10.
10
16> Z=15.
15
17> M = <<X:3, Y:7, Z:6>>.
<<66,143>>

範例:16 位元 RGB

如果 16位元的 RGB 顏色,R 佔用5位元,G 佔用 6 個位元, B 佔用 5 個位元。

19> Red = 2.
2
20> Green=61.
61
21> Blue=20.
20
22> Mem = <<Red:5,Green:6,Blue:5>>.
<<23,180>>
23> <<R1:5,G1:6,B1:5>> = Mem.
<<23,180>>
24> R1.
2
25> G1.
61
26> B1.
20

位元語法表示式

位元語法表示格式如下,每個 Ei 是四種形式之一。位元的總個數必須要是 8 的倍數。建構 binary 時,Value 必須要是已繫結的變數、字串、整數、浮點數、binary。而用在 pattern matching 時,Valued 可以是已繫結或未繫結的變數、字串、整數、浮點數、binary。

TypeSpecifierList 是型別指定子清單,一個用減號分隔項目的list,End-Sign-Type-Unit

<<>>
<<E1, E2, ..., En>>

Ei = Value |
    Value:Size|
    Value/TypeSpecifierList |
    Value:Size/TypeSpecifierList

@type End = big|little|native

@type Sign = signed | unsigned

@type Type = integer | float | binary

@type Unit = 1|2|...255

End 跟機器有關,預設值為 big,這是 endianess,當資料是 16#12345678,如果是 little-endian,要寫到從0x0000開始的記憶體位址時,就存為 16#78 16#56 16#34 16#12,如果是big-endian,在記憶體中就存為 16#12 16#34 16#56 16#78,最高位元組在位址最低位元。native 則表示是在執行時才由 CPU 決定。

以目前常見的CPU為例:INTEL X86、DEC VAX 使用 LITTLE-ENDIAN 設計;HP、IBM、MOTOROLA 68K 系列使用 BIG-ENDIAN 設計;POWERPC 同時支援兩種格式,稱為 BI-ENDIAN。

Sign 只用在 pattern matching,預設值為 unsigned
Type 預設值為 integer
Unit 的值由 Type 決定,如果 Type 是 integer 或 float,Unit 為 1,如果 Type 是 binary,Unit 則為 8。Size * Unit 的結果,就是整個 binary 的體積,總體積必須要是 8 的倍數。

27> {<<16#12345678:32/big>>, <<16#12345678:32/little>>, <<16#12345678:32/native>>, <<16#12345678:32>>}.
{<<18,52,86,120>>,
 <<120,86,52,18>>,
 <<120,86,52,18>>,
 <<18,52,86,120>>}

從範例可看出,這台機器是使用 little-endian。


參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

2014/2/23

用一行 list comprehension 解魔方陣

小朋友的學校給了一個特殊的魔方陣題目,基本上用列舉的方式,每一種可能發生的狀況慢慢列出來,然後排除不合理的狀況,就可以得出結果了。

題目:
空格是 1~14 正整數,數字不會重複出現,且要滿足直與橫的運算式。(我把空格都先加上了變數名稱)



A1 + A2 - A3 = A4
B1 + B2 - B3 = B4
C1 * C2 * C3 = C4

A1 + B1 - C1 = D1
A2 / B2 / C2 = D2
A3 + B3 + C3 = 15

計算:
如果要直接算,應該可以用 1 ~ 14 每個數字的正因數列表,來列出 A2 / B2 / C2 = D2 的所有狀況,另外再列出 A3 + B3 + C3 = 15 的所有狀況,然後再搭配 C1 C2 C3 = C4 應該就能找出解答。

不過我沒耐心慢慢去列舉,就想到其實列舉,並滿足某個條件,正是 list comprehension 最強大的用途。

第一版

L=[{A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2}||
     A1 <- lists:seq(1,14),
     A2 <- lists:seq(1,14),
     A3 <- lists:seq(1,14),
     A4 <- lists:seq(1,14),

     B1 <- lists:seq(1,14),
     B2 <- lists:seq(1,14),
     B3 <- lists:seq(1,14),
     B4 <- lists:seq(1,14),

     C1 <- lists:seq(1,14),
     C2 <- lists:seq(1,14),
     C3 <- lists:seq(1,14),
     C4 <- lists:seq(1,14),

     D1 <- lists:seq(1,14),
     D2 <- lists:seq(1,14),

     A1=/=A2,
     A2=/=A3,
     A3=/=A4,
     A4=/=B1,
     B1=/=B2,
     B2=/=B3,
     B3=/=B4,
     B4=/=C1,
     C1=/=C2,
     C2=/=C3,
     C3=/=C4,
     C4=/=D1,
     D1=/=D2,
     D2=/=A1,

     A1+A2-A3=:=A4,
     B1+B2-B3=:=B4,
     C1*C2*C3=:=C4,
     A1+B1-C1=:=D1,
     (A2 div B2) div C2=:=D2,
     A3+B3+C3=:=15
    ].

跑了幾分鐘還沒得到結果,所以就停掉,改第二版

K = lists:seq(1,14).
L=[{A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2}||
     A1 <- K,
     A2 <- K--[A1],
     A3 <- K--[A1,A2],
     A4 <- K--[A1,A2,A3],

     B1 <- K--[A1,A2,A3,A4],
     B2 <- K--[A1,A2,A3,A4,B1],
     B3 <- K--[A1,A2,A3,A4,B1,B2],
     B4 <- K--[A1,A2,A3,A4,B1,B2,B3],

     C1 <- K--[A1,A2,A3,A4,B1,B2,B3,B4],
     C2 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1],
     C3 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2],
     C4 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3],

     D1 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4],
     D2 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1],

     A1+A2-A3=:=A4,
     B1+B2-B3=:=B4,
     C1*C2*C3=:=C4,
     A1+B1-C1=:=D1,
     (A2 div B2) div C2=:=D2,
     A3+B3+C3=:=15
    ].

等了很久還是沒結果,判斷應該是前面列舉出來的結果太多了,改第三版

K = lists:seq(1,14).
L=[{A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2}||
     A1 <- K,
     A2 <- K--[A1],
     A3 <- K--[A1,A2],
     A4 <- K--[A1,A2,A3],
     A1+A2-A3=:=A4,

     B1 <- K--[A1,A2,A3,A4],
     B2 <- K--[A1,A2,A3,A4,B1],
     B3 <- K--[A1,A2,A3,A4,B1,B2],
     B4 <- K--[A1,A2,A3,A4,B1,B2,B3],
     B1+B2-B3=:=B4,

     C1 <- K--[A1,A2,A3,A4,B1,B2,B3,B4],
     C2 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1],
     C3 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2],
     C4 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3],
     C1*C2*C3=:=C4,

     D1 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4],
     D2 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1],

     A1+B1-C1=:=D1,
     (A2 div B2) div C2=:=D2,
     A3+B3+C3=:=15
    ].

測試結果

1> test:resolve().
[{7,14,8,13,10,4,5,9,6,1,2,12,11,3},
 {8,12,7,13,11,4,6,9,5,1,2,10,14,3}]

答案有兩組,但實際上驗算,會發現整數除法有問題,第一組答案是錯的。

因此,我們再加上一個條件,來排除整數除法的問題。

D2*C2*B2=:=A2

這就行了,最終再加上計算的時間統計

resolve() ->
    statistics(runtime),
    statistics(wall_clock),
    K = lists:seq(1,14),
    L=[{A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2}||
     A1 <- K,
     A2 <- K--[A1],
     A3 <- K--[A1,A2],
     A4 <- K--[A1,A2,A3],
     A1+A2-A3=:=A4,

     B1 <- K--[A1,A2,A3,A4],
     B2 <- K--[A1,A2,A3,A4,B1],
     B3 <- K--[A1,A2,A3,A4,B1,B2],
     B4 <- K--[A1,A2,A3,A4,B1,B2,B3],
     B1+B2-B3=:=B4,

     C1 <- K--[A1,A2,A3,A4,B1,B2,B3,B4],
     C2 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1],
     C3 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2],
     C4 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3],
     C1 * C2 * C3=:=C4,

     D1 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4],
     D2 <- K--[A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1],

     A1+B1-C1=:=D1,
     (A2 div B2) div C2=:=D2,
     D2*C2*B2=:=A2,
     A3+B3+C3=:=15
    ],
    {_, Time1} = statistics(runtime),
    {_, Time2} = statistics(wall_clock),
    io:format("runtime=~p wall_clock=~p ~n", [Time1, Time2]),
    L.

測試結果

1> test:resolve().
runtime=96284 wall_clock=104801
[{8,12,7,13,11,4,6,9,5,1,2,10,14,3}]

最後答案就是


===========

原本的計算時間花太久了,今天想到應該要先計算這兩個式子,才能有效在前面就把篩選的可能狀況減少

A2 / B2 / C2 = D2
C1 * C2 * C3 = C4

再修改程式,首先把 =:= 完全相等 換成 == 邏輯相等,也不用整數除法 div,改為 / ,接下來再調整運算的順序。

resolve() ->
    statistics(runtime),
    statistics(wall_clock),
    K = lists:seq(1,14),
    L=[{A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2}||
       A2 <- K,
       B2 <- K--[A2],
       C2 <- K--[A2,B2],
       D2 <- K--[A2,B2,C2],
       (A2 / B2) / C2==D2,

       C1 <- K--[A2,B2,C2,D2],
       C3 <- K--[A2,B2,C2,D2,C1],
       C4 <- K--[A2,B2,C2,D2,C1,C3],
       C1*C2*C3==C4,

       A3 <- K--[A2,B2,C2,D2,C1,C3,C4],
       B3 <- K--[A2,B2,C2,D2,C1,C3,C4,A3],
       A3+B3+C3==15,

       A1 <- K--[A2,B2,C2,D2,C1,C3,C4,A3,B3],
       A4 <- K--[A2,B2,C2,D2,C1,C3,C4,A3,B3,A1],
       A1+A2-A3==A4,

       B1 <- K--[A2,B2,C2,D2,C1,C3,C4,A3,B3,A1,A4],
       B4 <- K--[A2,B2,C2,D2,C1,C3,C4,A3,B3,A1,A4,B1],
       B1+B2-B3==B4,

       D1 <- K--[A2,B2,C2,D2,C1,C3,C4,A3,B3,A1,A4,B1,B4],
       A1+B1-C1==D1
      ],
    {_, Time1} = statistics(runtime),
    {_, Time2} = statistics(wall_clock),
    io:format("runtime=~p wall_clock=~p ~n", [Time1, Time2]),
    L.

測試結果很驚人,花掉的時間變得非常短。

1> test:resolve().
runtime=0 wall_clock=0
[{8,12,7,13,11,4,6,9,5,1,2,10,14,3}]
2> test:resolve().
runtime=15 wall_clock=15
[{8,12,7,13,11,4,6,9,5,1,2,10,14,3}]

結論,寫程式還是需要 follow 手算的想法,好的演算法,才能有效減少電腦計算的時間。

2014/2/20

erlang basics - list comprehension

list comprehension 是 erlang 裡面很重要的一項功能,它能有效縮減程式碼,讓程式更容易理解。quick sort wiki 收集了很多語言的實作,erlang 很神奇地以三行程式碼大勝。

語法

以下是數學在描述一個集合時,可列出屬於 N 的自然數集合,且大於 0 的所有 x,也就是所有正整數的集合。

{x | x∈N, x>0}

在 erlang 中,可用 list comprehension 語法,描述出類似的概念。 <- 是生成器,|| 的右邊,除了生成器之外,其他的都是約束條件(filter)

[X || X <- ListOfIntegers, X>0]

以下是 list comprehension 的一般形式,Qualifier 可以是生成器 generator 或是 filter

[X || Qualifier1, Qualifier2, ...]
generator: X <- L
filter: 元素的條件判斷

範例

以下可得到,所有正偶數平方的 list

[math:pow(X,2) || X <- ListOfIntegers, X>0, X rem 2 ==0]

以下可取得面積大於等於 10 的矩形 list。

[{area, H * W} || {rectangle, H, W} <- Shape, H*W>=10 ]

lists:map 與 list comprehension

list comprehension 可用來快速建立一組匹配的 list。

要得到 L 裡面所有元素的兩倍的 list,可以用 lists:map ,也可以直接用 list comprehension 取代處理。

(erlangotp@yaoclNB)5> L=[1,2,3,4,5].
[1,2,3,4,5]
(erlangotp@yaoclNB)6> lists:map(fun(X) -> 2*X end, L).
[2,4,6,8,10]
(erlangotp@yaoclNB)7> [2*X || X <- L].
[2,4,6,8,10]
map(F,L) -> [F(X) || X <- L]

tuple 元素的處理

當 list 裡的元素是 tuple 時,同樣也能使用 list comprehension,來處理。

(erlangotp@yaoclNB)8> Buy = [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 5}].
[{oranges,4},{newspaper,1},{apples,10},{pears,6},{milk,5}]
(erlangotp@yaoclNB)9> [{Name, 2*Number} || {Name, Number} <- Buy].
[{oranges,8},{newspaper,2},{apples,20},{pears,12},{milk,10}]

quick sort

quick sort wiki 收集了很多語言的實作,erlang 很神奇地以三行程式碼大勝。

qsort([]) -> [];
qsort([Pivot|T]) ->
    qsort([X || X <-T, X<Pivot]) ++ [Pivot] ++ qsort([X || X <-T, X>=Pivot]).

測試

(erlangotp@yaoclNB)3> L=[23,6,2,9,120,15].
[23,6,2,9,120,15]
(erlangotp@yaoclNB)4> test:qsort(L).
[2,6,9,15,23,120]

運作過程

  1. L = [23,6,2,9,120,15] 符合第二個子句,L 會被切割成 [Pivot|T],所以 Pivot = 23, T = [6,2,9,120,15]

  2. qsort([X || X <-T, X<Pivot]) 就等於 qsort([X || X <-[6,2,9,120,15], X<23]),也就是把 T 裡面所有小於 23 的元素都取出來產生一個新的 list,然後再放進 qsort 再處理一次,也就是 qsort([6,2,9,15])

  3. qsort([X || X <-T, X>=Pivot]) 就等於 qsort([X || X <-[6,2,9,120,15], X>=23]),也就是把 T 裡面所有大於等於 23 的元素都取出來產生一個新的 list,然後再放進 qsort 再處理一次,也就是 qsort([120])

  4. 第一次處理後的結果為 qsort([6,2,9,15]) ++ [23] ++ qsort([120])

  5. 然後再處理 qsort([6,2,9,15]),同樣符合第二個子句,Pivot = 6,T = [2,9,15]

  6. 結果為 qsort([2]) ++ [6] ++ qsort([9,15])

  7. qsort([2]) 同樣符合第二個子句,Pivot = 2,T = [],結果為 qsort([]) ++ [2] ++ qsort([])

  8. qsort([]) 符合第一個子句,結果為 []

因此整個運作的過程可寫成
qsort([23,6,2,9,120,15])
= qsort([6,2,9,15]) ++ [23] ++ qsort([120])
= qsort([2]) ++ [6] ++ qsort([9,15]) ++ [23] ++ [] + [120] + []
= [] + [2] + [] ++ [6] ++ [] ++ [9] ++ qsort([15]) ++ [23] ++ [] ++ [120] ++ []
= [] ++ [2] ++ [] ++ [6] ++ [] ++ [9] ++ [] ++ [15] ++ [] ++ [23] ++ [] ++ [120] ++ []
= [2,6,9,15,23,120]

畢氏定理

畢氏定理:直角三角形的邊長 {A,B,C},必須滿足兩股的平方和要等於斜邊的平方這個條件,A^2 + B^2 = C^2。

lists:seq(1,N) 可取得 1 到 N 所有正整數的 list。

pythag(N) 可以取得小於N,並滿足畢氏定理的所有正整數 {A,B,C} 的集合 list。

pythag(N) ->
    [ {A,B,C} ||
      A <- lists:seq(1,N),
      B <- lists:seq(1,N),
      C <- lists:seq(1,N),
      A+B+C =< N,
      A*A+B*B =:= C*C
    ].

測試

(erlangotp@yaoclNB)9> test:pythag(16).
[{3,4,5},{4,3,5}]
(erlangotp@yaoclNB)10> test:pythag(30).
[{3,4,5},{4,3,5},{5,12,13},{6,8,10},{8,6,10},{12,5,13}]
(erlangotp@yaoclNB)11> test:pythag(40).
[{3,4,5},
 {4,3,5},
 {5,12,13},
 {6,8,10},
 {8,6,10},
 {8,15,17},
 {9,12,15},
 {12,5,13},
 {12,9,15},
 {15,8,17}]

anagram 回文

perms 可取得一個英文字串,能找到的字母的所有排列組合。

perms([]) ->
    [[]];
perms(L) ->
    [[H|T] || H <- L, T <- perms(L--[H])].

測試

(erlangotp@yaoclNB)12> test:perms("123").
["123","132","213","231","312","321"]
(erlangotp@yaoclNB)13> test:perms("cats").
["cats","cast","ctas","ctsa","csat","csta","acts","acst",
 "atcs","atsc","asct","astc","tcas","tcsa","tacs","tasc",
 "tsca","tsac","scat","scta","sact","satc","stca","stac"]

運作過程

  1. perms("123") 因為字串就等同於 list,所以滿足第二個子句,有三種狀況
    1.1 H 為 "1",T 為 perms("23")
    1.2 H 為 "2", T 為 perms("13")
    1.3 H 為 "3", T 為 perms("12")

  2. perms("23") 滿足第二個子句,有兩種狀況
    2.1 H 為 "2", T 為 perms("3")
    2.2 H 為 "3", T 為 perms("2")

  3. perms("3") 滿足第二個子句,有一種狀況
    3.1 H 為 "3", T 為 perms(""), T 就滿足第一個子句,結果為 [[]],因此 [H|T] 就是 ["3"]

  4. 回到 2.1,H 為 "2", T 為 "3",因此 [H|T] 就是 ["23"],而2.2 的結果為 ["32"]

  5. 回到 1.1,H 為 "1",T 為 "23" 或 "32",結果為 ["123", "132"]

  6. 加上 1.2 與 1.3 的結果為 ["123","132","213","231","312","321"]

以自然順序建立 list

建立 list 最有效率的方式,就是把新的元素加入到 list 的頭部,我們常會看到有這樣的程式碼,這會走入 list,取出 list 的頭部,然計算得到 H1,最後將 H1 加入 Result 中,當輸入的資料耗盡,會滿足第二個條件,此函數會輸出 Result 結果。

some_function([H|T], ..., Result, ...) ->
    H1 = ... H ...,
    some_function(T, ..., [H1|Result], ...);
some_function([], ..., Result, ...) ->
    {..., Result, ...}.

注意的事項

  1. 增加元素一定要放在 list 的頭部
  2. 從頭部取出元素,處理後加入到另一個 list 的頭部,這會讓 Result list 跟輸入的 list 的元素順序相反。
  3. 如果順序一定要一樣,就呼叫 lists:reverse/1 進行反轉
  4. 反轉 list 要呼叫 lists:reverse/1,絕對不能用 List ++ [H],這會造成效能問題

從一個函數取得兩個 list

如果要寫一個函數,將 list 分為 奇數跟偶數 兩個 list,直觀的作法,就直接用 list comprehension 處理兩次。

odds_and_evens(L) ->
    Odds = [X || X<-L, (X rem 2) =:= 1],
    Evens = [X || X<-L, (X rem 2) =:= 0],
    {Odds, Evens}.

但 traverse list 兩次,並不是個好方法,應該改為 traverse 一次,然後根據 X rem 2 的 結果,將 H 放入不同的 result list 中,最後再以 lists:reverse 把順序反轉。

odds_and_evens_acc(L) ->
    odds_and_evens_acc(L, [], []).

odds_and_evens_acc([H|T], Odds, Evens) ->
    case (H rem 2) of
        1 -> odds_and_evens_acc( T, [H|Odds], Evens);
        0 -> odds_and_evens_acc( T, Odds, [H|Evens])
    end;

odds_and_evens_acc([], Odds, Evens) ->
    {lists:reverse(Odds), lists:reverse(Evens)}.

測試

(erlangotp@yaoclNB)1> test:odds_and_evens([1,2,3,4,5,6,7,8]).
{[1,3,5,7],[2,4,6,8]}
(erlangotp@yaoclNB)2> test:odds_and_evens_acc([1,2,3,4,5,6,7,8]).
{[1,3,5,7],[2,4,6,8]}

結語

當我們需要滿足某個條件的集合時,我們要先思考到在數學的集合表示式,然後再想到,以 list comprehension 來將這個集合實作出來。

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

2014/2/17

企業遊戲化:5年級、90後,一起玩出競爭新策略 - Gabe Zichermann & Joselin Linder

人們似乎天生就會被某些遊戲吸引,每一個人喜歡的類型不同,遊戲通常都是由幾個重複的動作組成,每一個遊戲都有一些核心的程序,玩家必須要定時或是每一次去玩都得要重複那個動作,而人類為了消耗閒暇的時間,就會讓自己沈迷習慣於幾個重複的動作,美其名為休閒,但實際上往往有很多人會慢慢地被遊戲侵蝕,反客為主,把平常的時間都用在遊戲上,也就是上癮的症狀。

因應這種狀況,有越來越多企業,反過來利用遊戲化,來正向地增加參與者的投入感,能以更認真的心情,去進行企業希望某些對象花時間用心去做的一些事情。這本書將遊戲化的對象分成三個部份討論,分別是員工、客戶與群眾外包。

另外有一個重點,就是對象的人數必須要夠多,因為遊戲化需要消耗一些時間跟人力的成本去設計,如果對象不夠多,可能就不需要太過於精心設計這些遊戲,也沒辦法設計太過複雜的遊戲,畢竟企業投資也需要看成效,參與的人不夠多,玩家之間對抗的心理也會打折扣。

這就有點像是開心農場、candy crush爆紅的感覺,因為身邊的人都在玩,facebook 的朋友們都在上面競爭進度,慢慢地病毒式擴散出去,讓越來越多人去參與這個遊戲跟活動,但畢竟開心農場偷菜這種簡單又重複的東西,也會讓人厭倦,candy crush 越來越難的關卡,也慢慢地讓人卻步,因此要能有一款遊戲、一種方式,可以讓人長時間持續投入在其中,並不容易。

關於員工的部份,又可以分成兩個部份:一個是對內部的員工、一個是要招募員工。

針對內部員工,遊戲化可讓員工認同公司的文化,增強向心力,並引發工作上的創意,還有讓員工身心健康、教育訓練,也可以使用遊戲的方法。

範例:Ekins是 Nike 的一個九天訓練營,以故事的方式告訴員工公司的核心價值,甚至在活動後讓參與者可在腿上做勾形刺青,這是一種認同感。

範例:目標百貨利用遊戲機制,將結帳畫面做成遊戲的結果,鼓勵員工以更有效率的方式結帳,原本無聊的結帳動作,變成了一種良性競爭。

範例:MS 語言品質遊戲,讓世界各地的員工參與產品的翻譯檢查,以遊戲化的方式,讓員工主動參與,在出貨前找出有問題的翻譯。

範例:IDEOS 以角色扮演的遊戲體驗,讓設計師掌握整體的流程,進而提出更好更以使用者為導向的產品設計。

範例:美國陸軍以一款遊戲,吸引新兵從軍。

範例:google 以遊戲的方式,將招募資訊隱藏在裡面。

範例:萊雅以 Reveal 遊戲,讓學生全盤體驗化妝品產業中各項工作的內容,公司內部還能以遊戲的結果,來招募員工。

範例:萬豪酒店以遊戲體驗酒店內的工作,在遊戲結束,按鈕後就可以看到五萬個工作機會。

範例:國防採購大學採購弊端指標遊戲,教導員工發現弊端的方法。

面向客戶,遊戲化的方法就是抓住客戶眼球的方式,而且還需要持續,讓客戶能不斷回流,延續企業生命。

範例:foursqure 打卡遊戲,發出一些特別的徽章給使用者,例如玩家上場徽章、學校之夜徽章、某個地點的市長。

範例:Nike+, FuelBand 將運動資料記錄起來,創造以回饋為基礎的遊戲化體驗。

範例:Element Bar 是客製化的營養棒,使用者可在網站上設計 12 條營養棒,以 facebook 贈送的方式,吸引客戶的眼球。

群眾外包是藉由團隊的力量,大家一起合作完成一些事情,而這個特別的團隊,是在網路上的虛擬世界中得來的。

範例:Foldit是折疊蛋白質的遊戲,玩家花了10天就解決了科學家15年沒辦法解碼的酵素,用來研究 HIV。這些玩家大多沒有正式的數學或科學背景。

範例:StackOverflow 讓群眾回答問題,不像是 Yahoo 知識家單純地鼓勵用戶某種行為,造成回答的品質低下,StackOverflow是一套以同儕推動的聲譽系統,由其他人投票決定你的聲譽,有特別成就的人還有徽章。

範例:CNN 提出 iReport 公民新聞,授權公民記者收集獨家報導。

範例:KickStarter 讓所有人可以在上面提出構想,籌措資金,這也是驗證自己想法的一個好方式。

以下是幾種能讓人持續下去的方法,遊戲需要一個好方式建立持久的參與熱情。

  1. 清楚界定什麼是練功
    練功是某種簡單的動作,使用者要定期重複去做,日常生活中,打卡、回覆客服電話、參加例行會議都算是練功。在 Instagram 就是上傳相片,在郵差就是送信,但這些過程可能包含好幾個動作,使用者練功熟悉後就會成為習慣。
  2. 建立強大的參與循環
    提供情感動機 -> 社群性的行動號召 -> 激勵使用者再參與 -> 提供明顯的進展與獎賞 的循環。
  3. 讓內容時時保持新鮮
    以符合成本效益的方式維持內容新鮮,要找到方法,讓使用者彼此創造有趣的經驗。
  4. 善用有意義的誘因
    例如 RecyleBank 以 RFID 記錄回收項目,回收者可取得虛擬貨幣,向合作店家兌換商品、折扣,也能透過回收成就的分享,激發對成就感的需求。
  5. 針對個人需求,滿足掌控感
    Zamzee 健身記錄每個人的活動資訊,每天上傳後,進行統計,系統會不斷提供徽章及其他機制,鼓勵使用者持續維持,保持進步。
  6. 創造持續的學習機會
    Codecademy利用遊戲教導大家寫程式,在遊戲的過程中,完成學習。
  7. 讓忠誠度帶來獲利
    航空公司以里程兌換的方式,鼓勵使用者回流,頂級客戶可以不需要付費,享受航空公司提供的服務。

人們參與遊戲後,所得到的報酬,並不是單純只能給予金錢就好了,事實上金錢還有可能造成副作用,人們可以得到的報酬,可以是在虛擬世界的徽章、名聲,甚至是在 facebook 上朋友的讚,都是一種報酬,只要能讓玩家產生愉快的感覺,這樣的報酬就有作用,而且對企業來說,要消耗的實質成本很低。

企業遊戲化:5年級、90後,一起玩出競爭新策略

2014/2/14

erlang basics - function

測試 function 功能,可以改使用 erlide,因為 function 要放在 module 裡面測試,雖然一視窗編輯 erl 檔,另一邊用 erl 的 c(test_module) 也可以動態更新程式碼,使用 erlide 可以省去一些敲打指令的時間。

子句

io:format

io:format 有幾個格式化參數可以使用

~s 列印字串
~p 以美化方式列印,超過一行會自動分行
~w 列印 terms 的原始型態
~n 換行

%% test_module.erl

-module(test_module).

-export[hello/0].
-export[hello/1].

hello(From) ->
        io:format( "~s:Hello world~n", [From] ),
        io:format( "~p:Hello world~n", [From] ),
        io:format( "~w:Hello world~n", [From] ).

hello() ->
        hello("").
1> test_module:hello().
:Hello world
[]:Hello world
[]:Hello world
ok
2> test_module:hello("test").
test:Hello world
"test":Hello world
[116,101,115,116]:Hello world
ok

用 pattern matching 在多個子句中進行條件判斷

函數可以有多個子句,每個字句以 ; 分號隔開,最後以 . 句號結尾。

erlang 會自動由上至下逐一進行 pattern matching,一旦有模式符合了,下面其他的模式就不會再進行比對,如果都不匹配,那就會產生 function_clause 異常。

% test_module.erl

either_or_both(true, B) ->
    true;
either_or_both(A, true) ->
    true;
either_or_both(false, false) ->
    false.

只要符合 pattern,就會套用到該子句的 expressions。

1> test_module:either_or_both(true, 123).
true
2> test_module:either_or_both(true, true).
true

guardian clause

在上面的例子中,test_module:either_or_both(true, 123) 後面的 123 應該是異常的參數,可以附帶 guardian clause: when 增加檢查的條件。

% test_module.erl

either_or_both(true, B) when is_boolean(B) ->
    true;
either_or_both(A, true) when is_boolean(A) ->
    true;
either_or_both(false, false) ->
    false.
1> test_module:either_or_both(true, 123).
** exception error: no function clause matching
                    test_module:either_or_both(true,123) (test_module.erl, line 17)
2> test_module:either_or_both(true, true).
true

gardian clause 中可以使用 is_boolean()、is_atom()、is_integer() 等等判斷的函數,也可以使用 + - * / ++ ,也可使用部份 BIF,例如 self(),但不能使用自己定義的函數或是其他module裡的函數。

合法的 gardian clause 為

  1. atom: true
  2. 其他 term 與已繫結的變數: false
  3. 呼叫 guard predicate,這一些 BIF
  4. term 比較結果
  5. 算術表示式
  6. boolean 表示式
  7. short-circuit boolean 表示式: andalso, orelse

guard predicate BIFs:

  1. is_atom(X)
  2. is_binary(X)
  3. is_constant(X)
  4. is_float(X)
  5. is_function(X)
  6. is_function(X, N): X 是否為具有 N 個引數的函數
  7. is_integer(X)
  8. is_list(X)
  9. is_number(X)
  10. is_pid(X)
  11. is_port(X)
  12. is_reference(X)
  13. is_tuple(X)
  14. is_record(X, Tag): X 是否為型別為 Tag 的 record
  15. is_record(X, Tag, N): X 是否為型別為 Tag 的 record, 大小為 N

以下這些都是已經不使用的 guard predicate BIFs

  1. abs(X)
  2. element(N,X): X 的元素 N,X 必須為 tuple
  3. float(X)
  4. hd(X): list X 的 head element
  5. length(X): list X 的長度
  6. node(): 目前的節點
  7. node(X): X 被建立的節點,X 是 process/identifier/reference/port
  8. round(X): 將 number X 轉成整數
  9. self(): 目前 process 的 pid
  10. size(X): X 的大小,X 可以為 tuple 或 binary
  11. trunc(X): 截斷 X 成為整數
  12. tl(X): list X 的尾部

variable scope

erlang 習慣會在 tuple 的第一個元素,以 atom 標記此資料的識別標籤。

變數的值雖然是不可異動的,但作用範圍是在該子句之中,一直到分號或句號就結束。

area({circle, Radius}) ->
    Radius * Radius * math:pi();
area({square, Side}) ->
    Side * Side;
area({rectangle, Height, Width}) ->
    Height * Width.
3> test:area({circle,2.1}).
13.854423602330987

case XX of YYY end.

將剛剛的 area 以 case XX of YYY end. 的方式改寫,程式變得比較精簡,但大多數的programmer習慣使用上面的方式,即使要寫三次 area,普遍認為上面的方式可讀性較高。

area(Shape) ->
    case Shape of
        {circle, Radius} ->
            Radius * Radius * math:pi();
        {square, Side} ->
            Side * Side;
        {rectangle, Height, Width} ->
            Height * Width
    end.

也可以將剛剛的 either_or_both 以 case of 改寫

either_or_both({A,B}) ->
    case {A, B} of
        {true, B} when is_boolean(B) ->
            true;
        {A, true} when is_boolean(A) ->
            true;
        {false, false} ->
            false
    end.

if

if 是 case of 的簡化形式,不針對特定值判斷,也不包含 pattern,如果只依靠 guardian clause 進行子句選擇時,就可以使用 if。

因為 if 只是 case of 的簡化,所以可以用 case of 改寫 if。

sign(N) when is_number(N) ->
    if
        N > 0 -> positive;
        N < 0 -> negative;
        true  -> zero
    end.

sign1(N) when is_number(N) ->
    case N of
        _ when N > 0 -> positive;
        _ when N < 0 -> negative;
        _ when true  -> zero
    end.

erlang 沒有 if-then-else

erlang 沒有 if-then-else ,只能用 case of 寫。

test_either_or_both({A,B}) ->
    case either_or_both({A,B}) of
        true -> io:format("true...");
        false -> io:format("false...")
    end.

逗號與分號

Erlang 程式段落是由幾個子句構成,子句之間會看到逗號 ( , ) 、分號 ( ; ) 及句號 ( . ) ,一個完整的程式段落是以句號結尾:例如,前面看到的模組定義,以及函數定義。

逗號代表 and ,所以程式段落和防衛式都可以是用逗號分隔很多句子。

分號代表 or ,同一函數的數個規則之間以分號分隔。前面提到的 if .. end 、 case ... end 、 try ... end 、和 receive ... end 等等,許多條件判斷規則之間也是用分號分隔。

and 比 or 有較高優先權,換句話說,就是逗號比分號有較高優先權。

fun

作為現有函數別名的 fun

如果要引用 module 的某個函數,並告知程式其他部份,可以呼叫這個函數,就要建立 fun。

fun test:either_or_both/1 可以指定給變數,或是直接放在 yesno 的呼叫參數中。

yesno(F) ->
    case F({true, false}) of
        true  -> io:format("yes~n");
        false -> io:format("no~n")
    end.

然後在 erl console 裡面

(erlangotp@yaoclNB)15> H= fun test:either_or_both/1.
#Fun<test.either_or_both.1>
(erlangotp@yaoclNB)16> H({true, false}).
true
(erlangotp@yaoclNB)17> test:yesno(H).
yes
ok
(erlangotp@yaoclNB)18> test:yesno(fun test:either_or_both/1).
yes
ok

匿名函數 lambda

fun () -> 0 end

這就是個最簡單的匿名函數,fun開頭,end 結尾,匿名函數的作用,是要綁訂到變數或是直接當作參數傳給其他函數使用。

(erlangotp@yaoclNB)20> test:yesno(fun ({A,B}) -> A or B end).
yes
ok

lists:map lists:filter

lists:map(F, L) 可以將 fun F 套用在 L 的每一個元素上
lists:filter(P, L) 可以將 L 的每一個元素,以 P(E) 的方式檢查是否為 true,結果為 true 就保留在結果的 list 中

(erlangotp@yaoclNB)23> L=[1,2,3,4].
[1,2,3,4]
(erlangotp@yaoclNB)24> Double = fun(X) -> X*2 end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)25> lists:map(Double, L).
[1,4,6,8]


(erlangotp@yaoclNB)27> Even = fun(X) -> (X rem 2) =:=0 end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)28> Even(8).
true
(erlangotp@yaoclNB)29> Even(7).
false
(erlangotp@yaoclNB)30> lists:map(Even, L).
[false,true,false,true]
(erlangotp@yaoclNB)31> lists:filter(Even, L).
[2,4]

傳出 fun 的函數

以寫程式的角度來看,這就是要寫出一個可以產生 function 的 fun,用這個 fun 可以產生出很多邏輯類似的 function。

MakeTest 是個產生 function 的 fun

(erlangotp@yaoclNB)32> Fruit = [apple, pear, orange].
[apple,pear,orange]
(erlangotp@yaoclNB)33> MakeTest = fun(L) -> (fun(X) -> lists:member(X,L) end) end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)34> IsFruit = MakeTest(Fruit).
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)35> IsFruit(pear).
true
(erlangotp@yaoclNB)36> IsFruit(dog).
false
(erlangotp@yaoclNB)37> lists:filter(IsFruit, [dog, pear, bear, apple]).
[pear,apple]

fun(X) -> X*Times end 是 X 的函數,而 Times 是外面的 fun(Times) 傳進來的。

(erlangotp@yaoclNB)38> Mult = fun(Times) -> (fun(X) -> X*Times end) end.
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)39> Triple = Mult(3).
#Fun<erl_eval.6.80484245>
(erlangotp@yaoclNB)40> Triple(3).
9

這就是一種 closure:closure 通常是指 fun ... end 的內部引用的變數,在 fun 外面進行數值綁定的情況。

自己訂做一個 for 迴圈

當我們呼叫 for(1, 10, F) 時,會跟第二個子句符合,所以會變成
[F(1) | for(2,Max,F) ]
接下來會再往下展開
[F(1), F(2) | for(3,Max,F) ]
持續下去,就會得到這個 list
[F(1), F(2), F(3), ..., F(10) ]

for(Max, Max, F) ->
    [F(Max)];
for(I, Max, F) ->
    [F(I)| for(I+1, Max, F)].
(erlangotp@yaoclNB)42> test:for(1,10,fun(X) -> X end ).
[1,2,3,4,5,6,7,8,9,10]
(erlangotp@yaoclNB)43> test:for(1,10,fun(X) -> X*2 end ).
[2,4,6,8,10,12,14,16,18,20]

何時使用較高次方的函數

「較高次方的函數」就是剛剛提到的把函數當作參數使用,或是將 fun 當作函數的回傳值,Joe Armstrong 在書本裡說,實務這些技術不常用到。

  1. lists:map/2、filter/2、partition/2 這些BIF很常用到,幾乎可以認定為 erlang 語言的一部分
  2. 作者很少寫出像剛剛的 for 迴圈的自訂控制,反而常常會呼叫標準函式庫裏的較高次方函數。
  3. 作者很少寫出傳出 fun 的函數,寫出100個module,大概只有 1~2 個會使用這個技巧

異常與try/catch

異常可視為函數的另一種返回形式,異常會不斷地往上返回到呼叫者,直到被 catch 或是抵達 process 呼叫的起點(這時 process 便會 crash)為止。

erlang 的異常分三類

  1. error: 執行時異常,在發生除以0的錯誤、pattern matching 失敗、找不到匹配的函數子句時觸發。一旦錯誤造成 process crash,就會紀錄到 erlang error log 中。

     erlang:error(Reason)

    通常不需要在程式中拋出 error,但在撰寫 library 時,適時拋出 badarg 異常卻是個好習慣。

  2. exit: 通常用於通報「process即將停止」。他會迫使 process crash 的時候,將原因告知其他 processes,因此一般不 catch 這類異常。在 process正常終止時,也會使用 exit,他會命令 process 退出,並通報「任務結束、一切正常」。不管哪一種情況,process 因exit而終止,都不算是意外事件,也不會紀錄到 erlang error log 中。

     exit(Reason)

    exit(normal) 所拋出的異常不會被捕獲,該process會正常結束。

  3. throw: 此異常用於處理用戶自定義的情況,可以用 throw 來通報你的函數遇到了某種意外(例如文件不存在或遇到了非法輸入),可利用throw來完成非局部返回或是用於跳出深層遞迴。如果process沒有catch此異常,就會轉變成一個原因為 nocatch 的 error,迫使 process 終止並紀錄到 erlang error log 中。

     throw(SomeTerm)

使用 try ... of ... catch ... after ... end

try
    some_unsafe_function()
catch
    oops        -> got_throw_oops;
    throw:Other    -> {got_throw, Other};
    exit:Reason    -> {got_exit, Reason};
    error:Reason ->{got_error, reason}
end

一般狀況下,不應該去 catch exit 與 error,這可能會掩蓋系統的錯誤。

如果需要捕獲所有東西,可以用以下的寫法處理

try Expr
catch
    _:_            -> got_some_exception
end

如果要區分,正常情況跟異常情況做不同的處理時,可用以下寫法,在正常情況繼續處理,異常時,列印錯誤訊息並退出。這裡的 of 跟 catch 一樣,無法受到 try 的保護,of 與 catch 裡面的子句的異常,會傳播到 try 表達式之外。

try
    some_unsafe_function()
of
    0 -> io:format("nothing to do~n");
    N -> do_something_with(N)
catch
    _:_ -> io:format("somethin wrong~n")
end

after 區塊可確保 try, of, catch 全部都執行過後,才會執行,而且一定會執行。包含在 try 中拋出異常,或是在 of, catch 中拋出了新的異常,這些異常會先儲存起來,在 after 處理過後,再重新被拋出。如果 after 裡面又拋出了異常,拋出的異常就會取代先前的異常,原先的異常會被丟棄。

{ok, FileHandle} = file:open("foo.txt", [read]),
try
    do_something_with_file(FileHandle)
after
    file:close(FileHandle)
end

範例

generate_exception 產生所有可能的錯誤,catcher用來測試是否可以抓住他們。

-module(try_test).
-export([generate_exception/1, demo1/0]).

generate_exception(1) -> a;
generate_exception(2) -> throw(a);
generate_exception(3) -> exit(a);
generate_exception(4) -> {'EXIT', a};
generate_exception(5) -> erlang:error(a).

demo1() ->
    [catcher(I) || I <- [1,2,3,4,5]].

catcher(N) ->
    try generate_exception(N) of
        Val -> {N, normal, Val}
    catch
        throw:X -> {N, caught, thrown, X};
        exit:X -> {N, caught, exited, X};
        error:X -> {N, caught, error, X}
    end.

測試

(erlangotp@yaoclNB)14> try_test:demo1().
[{1,normal,a},
 {2,caught,thrown,a},
 {3,caught,exited,a},
 {4,normal,{'EXIT',a}},
 {5,caught,error,a}]

改進錯誤訊息

使用 erlang:error 可改進錯誤訊息的品質。

例如呼叫 math:sqrt(-10) 會得到錯誤

1> math:sqrt(-10).
** exception error: an error occurred when evaluating an arithmetic expression
     in function  math:sqrt/1
        called as math:sqrt(-10)

我們可以用一個函數,改進錯誤訊息

sqrt(X) when X<0 ->
    erlang:error({squareRootNegativeArgument, X});
sqrt(X) ->
    math:sqrt(X).

測試

2> try_test:sqrt(-10).
** exception error: {squareRootNegativeArgument,-10}
     in function  try_test:sqrt/1 (d:/projectcase/erlang/erlangotp/src/try_test.erl, line 34)

try ... catch 的程式風格

一般當函數沒有狀態時,應該回傳 {ok, Value} 或是 {error, Reason} 這樣的值。

只有兩種方式可以呼叫此函數

case f(X) of
    {ok, Val} ->
        do_something_with(Val);
    {error, Why} ->
        %% process error
end.

或是以下這種方式,但會在 f(X) 回傳 {error, ...} 時,傳出一個例外

{ok, Val} = f(X),
do_something_with(Val);

通常我們應該撰寫程式處理自己的錯誤

try myfunc(X)
catch
    throw:{thisError, X} -> ...
    throw:{someOtherError, X} -> ...
end

stack trace

stack trace 就是異常發生時,從 stack 頂部到所有呼叫的逆向順序列表,呼叫 erlang:get_stacktrace() 可查看目前這個 process 最近拋出的異常的 stack trace。每一個函數都會以 {Module, Function, Args} 的形式表示,其中 Module 與 Function 是 atom,而 Args 可能是函數的元數,或是函數被呼叫時的參數列表。

如果呼叫 erlang:get_stacktrace() 後得到一個空 list,就表示沒有發生任何異常。

重拋異常

檢視異常之後,再判斷是否要進行補捉,必要時可先捕捉異常,再以 erlang:raise(Class, Reason, Stacktrace) 重新拋出。這裡的 Class 必須是 error、exit 或 throw,Stacktrace 則應該來自 erlang:get_stacktrace()。

try
    do_something()
catch
    Class:Reason ->
        Trace = erlang_getstacktrace(),
        case analyze_exc(Class, Reason) of
            true -> handle_exc(Class, Reason, Trace);
            false-> erlang:raise(Class, Reason, Trace)
        end
end

舊版 erlang 支援的 catch

catch 在老的程式碼中很常見,寫法為 catch Expression,如果可取得結果,就以此為結果,如果發生異常,就將捕獲的異常作為 catch 的結果。

(erlangotp@yaoclNB)1> catch 2+2.
4
(erlangotp@yaoclNB)2> catch throw(foo).
foo
(erlangotp@yaoclNB)3> catch exit(foo).
{'EXIT',foo}
(erlangotp@yaoclNB)4> catch foo=bar.
{'EXIT',{{badmatch,bar},[{erl_eval,expr,3,[]}]}}
(erlangotp@yaoclNB)5>

對於 error,得到的是包含異常本身與 stacktrace 的 tuple,這種設計看來簡單,但卻讓程式無法判斷究竟發生了什麼,無法進行後續處理,要避免使用舊寫法的 catch

捕捉例外的另一個方式是直接使用 catch,但是跟前一個例子比較結果後,會發現第二個 throw(a) 的部份沒有捕捉到,而且 3,4,5 的部份,也不能知道精確的資訊。

demo2() ->
    [{I, (catch generate_exception(I))} || I <- [1,2,3,4,5] ].
(erlangotp@yaoclNB)15> try_test:demo2().
[{1,a},
 {2,a},
 {3,{'EXIT',a}},
 {4,{'EXIT',a}},
 {5,
  {'EXIT',{a,[{try_test,generate_exception,1,
                        [{file,"d:/projectcase/erlang/erlangotp/src/try_test.erl"},
                         {line,16}]},
              {try_test,'-demo2/0-lc$^0/1-0-',1,
                        [{file,"d:/projectcase/erlang/erlangotp/src/try_test.erl"},
                         {line,31}]},
              {try_test,'-demo2/0-lc$^0/1-0-',1,
                        [{file,"d:/projectcase/erlang/erlangotp/src/try_test.erl"},
                         {line,31}]},
              {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,573}]},
              {shell,exprs,7,[{file,"shell.erl"},{line,674}]},
              {shell,eval_exprs,7,[{file,"shell.erl"},{line,629}]},
              {shell,eval_loop,3,[{file,"shell.erl"},{line,614}]}]}}}]

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

2014/2/9

erlang basics - module, variable, pattern matching

module

module 是程式碼的容器,每個 module 的名字都是 global 唯一的 atom。

呼叫函數

local 呼叫是指呼叫同一個 module 裡的函數
remote 呼叫是指呼叫其他 module 裡的函數

以 module:fun 的方式呼叫函數

erlang 的函數數量稱為 arity 元數,函數的全名必須要包含 arity,例如 reverse/1、lists:reverse/1、lists:reverse/2

18> lists:reverse([3,2,1]).
[1,2,3]
19> lists:reverse([5,6,7],[3,2,1]).
[7,6,5,3,2,1]

BIF: built-in function

erlang 內建一套標準函式庫,散布在各個 module 中,例如: erlang、lists、dict、array。

BIF 會自動被 include,不需要 include_lib,例如 self() 其實就是 erlang:self(),因為自動引用,所以可以省略 erlang:

自建 module

建立 test_module.erl 檔案

%% test_module.erl
-module(test_module).

-export[hello/0].
-export[hello/1].

hello(From) ->
        io:format( "~s:Hello world~n", [From] ).

hello() ->
        hello("").

可在 linux shell 裡面直接以 erlc 編譯,會產生 test_module.beam 檔案,可使用 -o 指定 .beam 輸出的目錄。

> erlc test_module.erl
> erlc -o ebin test_module.erl

在 erl shell 裡面,erlang 會直接嘗試載入 test_module,但如果有修改 test_module.erl,也可以直接以 c(test_module) 編譯並載入更新後的 test_module

1> test_module:hello().
 Hello world
ok
2> test_module:hello("test").
test:Hello world
ok
3> c(test_module).
{ok,test_module}
4> test_module:hello("test").
test:Hello world
ok
5> test_module:hello().
:Hello world
ok

code:get_path(). 可將所有 shell 會自動搜尋的路徑列印出來

4> code:get_path().
[".","/usr/local/lib/erlang/lib/kernel-2.16.4/ebin",
 "/usr/local/lib/erlang/lib/stdlib-1.19.4/ebin",
 "/usr/local/lib/erlang/lib/xmerl-1.3.5/ebin",
 "/usr/local/lib/erlang/lib/wx-1.1.1/ebin",
 "/usr/local/lib/erlang/lib/webtool-0.8.9.2/ebin",
 "/usr/local/lib/erlang/lib/typer-0.9.5/ebin",
 "/usr/local/lib/erlang/lib/tv-2.1.4.10/ebin",
 "/usr/local/lib/erlang/lib/tools-2.6.13/ebin",
 "/usr/local/lib/erlang/lib/toolbar-1.4.2.3/ebin",
 "/usr/local/lib/erlang/lib/test_server-3.6.4/ebin",
 "/usr/local/lib/erlang/lib/syntax_tools-1.6.12/ebin",
 "/usr/local/lib/erlang/lib/ssl-5.3.2/ebin",
 "/usr/local/lib/erlang/lib/ssh-3.0/ebin",
 "/usr/local/lib/erlang/lib/snmp-4.25/ebin",
 "/usr/local/lib/erlang/lib/sasl-2.3.4/ebin",
 "/usr/local/lib/erlang/lib/runtime_tools-1.8.13/ebin",
 "/usr/local/lib/erlang/lib/reltool-0.6.4.1/ebin",
 "/usr/local/lib/erlang/lib/public_key-0.21/ebin",
 "/usr/local/lib/erlang/lib/pman-2.7.1.4/ebin",
 "/usr/local/lib/erlang/lib/percept-0.8.8.2/ebin",
 "/usr/local/lib/erlang/lib/parsetools-2.0.10/ebin",
 "/usr/local/lib/erlang/lib/otp_mibs-1.0.8/ebin",
 "/usr/local/lib/erlang/lib/os_mon-2.2.14/ebin",
 "/usr/local/lib/erlang/lib/orber-3.6.26.1/ebin",
 "/usr/local/lib/erlang/lib/odbc-2.10.18/ebin",
 "/usr/local/lib/erlang/lib/observer-1.3.1.2/ebin",
 "/usr/local/lib/erlang/lib/mnesia-4.11/ebin",
 [...]|...]

variable

erlang 的變數必須以大寫的字母開頭,變數中每個單字的開頭,都要大寫(CamelCase)。

變數也可以用 _ 底線開頭,但以底線開頭的變數,就不會觸發沒有使用到這個變數的編譯器的警告,所有未被使用的變數,都會被優化掉。

Name
ShoeSize
ThisIsAVariable
_Name
_ShoeSize

底線變數有兩個用途

  1. 為我們不想用到的變數命名,例如寫 open(File, Mode) 比寫 open(File, ),程式更具有可讀性。
  2. 為了除錯的目的
    some_func(X) ->
     {P,Q} = some_other_func(X),
     io:format("Q= ~p~n", [Q]),
     P.
    如果將io:format 改為註解,編譯時,就會發生 Q 沒有使用的警告
    some_func(X) ->
     {P,Q} = some_other_func(X),
     %% io:format("Q= ~p~n", [Q]),
     P.
    因此我們把 Q 改為 _Q,就可以避免產生警告訊息
    some_func(X) ->
     {P,_Q} = some_other_func(X),
     io:format("_Q= ~p~n", [_Q]),
     P.

single assignment

當變數被指定數值時,該變數在程式作用的範圍中,就不會變動。

在 shell 中,只要這個 shell 還在運作,變數就不能異動,一旦異動,就會收到 pattern matching 的錯誤。但可以使用 f() 把變數的 binding 值忘記,然後就可以再次指定新值。

6> _Name=2.
2
7> _Name=3.
** exception error: no match of right hand side value 3
8> f(_Name).
ok
9> _Name=3.
3

pattern matching

這是 erlang 最重要的功能,pattern matching 是加強版的 value binding,有以下的用途:

  1. 選擇程式控制分支
  2. 完成變數binding
  3. 拆解資料結構

= 就是 pattern matching 的運算符號

11> {A,B,C}={2014,"NewYear",ok}.
{2014,"NewYear",ok}
12> A.
2014
13> C.
ok
14> B.
"NewYear"

15> {rect, Width, Height} = {rect, 100, 20}.
{rect,100,20}
16> Width.
100

{rect, Width, Height} 利用 rect 這個 atom 作為 tuple 資料的標籤,可在 pattern matching 時,馬上確認並檢查此資料來源的正確性。

don't care pattern: _

當我們有個 Users 的資料結構,裡面存有兩個 users,在這樣複雜的結構下,我們要怎麼確認第一個人符合我們設定條件呢?

用以下的方式,可確認第一個人 LastName 是不是 "Chen",同時取得那個人的 Firname 與 Age。

25> Users = [{person, [{name, "John", "Chen"}, {age, 30}, {tags, [math, male]}]}, {person, [{name, "James", "Chen"}, {age, 33}, {tags, [cs, female]}]} ].
[{person,[{name,"John","Chen"},{age,30},{tags,[math,male]}]},
 {person,[{name,"James","Chen"},
          {age,33},
          {tags,[cs,female]}]}]

26> [{person, [{name, Firstname, "Chen"}, {age, Age}, {tags, _}]} | _ ] = Users.
[{person,[{name,"John","Chen"},
          {age,30},
          {tags,[math,male]}]}]
27> Firstname.
"John"
28> Age.
30

可將 Rest 直接 bind 到後面三個元素的list。

6> [1,2,3 | Rest] = [1,2,3,4,5,6].
[1,2,3,4,5,6]
7> Rest.
[4,5,6]

可以套用在 strings 上,因為 string 也是一種 list。也可以直接使用 ++ 並進行 pattern matching。

8> [$h, $t, $t, $p, $: | Url]="http://www.maxkit.com.tw".
"http://www.maxkit.com.tw"
9> Url.
"//www.maxkit.com.tw"

12> "http://" ++ Rest1 = "http://www.maxkit.com.tw".
"http://www.maxkit.com.tw"
13> Rest1.
"www.maxkit.com.tw"

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

理想國四部曲 - Lois Lowry

烏托邦是由托馬斯·摩爾的「烏托邦」一書中所寫的完全理想的共和國「烏托邦」而來,意思是理想完美的境界,然而為了維護這個理想中的的社會組織,組織內的成員通常需要犧牲某些個人的權力與私慾,才能造成一個完美的大同社會。

正因為是個人類的組織,就會有階級與領導者,在少數人掌握權力的烏托邦中,就有可能會因為人性的黑暗面,漸漸地被權力腐化,進而實施一些規則,表面上是維護烏托邦的組織,實際上卻是剝奪了許多平民的基本生活需求,反而成了違反人性需求的規則,並以教育洗腦的方式,宣傳這些作法都是必要的,因為大家都夢想在這樣的完美世界中生活。

也因此有了「反烏托邦」的概念,反烏托邦主義的代表作是英國赫胥黎著作的「美麗新世界」,英國喬治·歐威爾的「動物農莊」和「一九八四」,理想國四部曲是寫給青少年讀的「反烏托邦」的小說,因此故事裡的主角年紀都很輕,而且都掌握了某些特殊的能力,且運用這些能力對抗這些不合理的社會。

記憶傳承人中的喬納思有超眼界的能力,能看到遠方的畫面,看到別人看不見的顏色

歷史刺繡人中的綺拉有預知的能力,透過刺繡,能呈現未來的畫面

森林送信人中的小麥(麥迪)有治癒的能力,除了能治療生物,最後還治療了整個大地

我兒佳比中的佳比有附身的能力,能潛入別人的內心,了解對方的感受。

從奇幻的角度來看這些特殊能力,讓人在讀的時候覺得有趣,還有中間穿插的一位惡的代表:交易大師,神秘而法力無邊,不管什麼都能換,但卻是用自己珍愛的另一個已經擁有卻又不自知的東西來換,但這些能力並不是作者強力著墨的要點。

故事的後記中提到,貫穿四部曲的概念就是「同理心」,這幾位主角雖身負異稟,但卻能揣摩別人的心意,並做出最好的決定,這其實是在告訴我們,就算你有最好的與眾不同的能力,在社會團體組織中,跟別人相處時,在下每一個決定時,如果能先替別人想一想,或許以別的方式來處理,會有更好的結果。

每一個小朋友都是從個人本位主義開始的,從想辦法餵飽自己開始的,漸漸地會發表自己的意見,爭奪自己的玩具,自己的空間,在逐漸社會化的過程中,「同理心」的練習能讓青少年更自然地融入社會與人群之中。

理想國四部曲從反烏托邦的概念,搭配著四位主角與眾不同的特殊能力,潛移默化讓讀者了解到,其實就是「同理心」緣故,讓這些主角選擇以最適當的方式運用能力,解決困難。

博客來 理想國四部曲 (套書)

如果有天真踏上一塊理想之地,希望那時候我們還沒忘記什麼是愛。2013童書小學年度之最《理想國四部曲》

Lois Lowry《記憶傳授人》

理想國四部曲 The Giver Quartet

從「記憶傳承人」談起

2014/2/5

砂之器 - 松本清張

最近看了松本清張的推理經典「砂之器」,故事內容是一位刑警今西為了追查刑案的兇手,抓住每一個微小的線索,到處追查兇手的過程。

關於「砂之器」,網路上的文章大都是在討論 1976 年拍的電影,2004與2011 年分別拍攝的日劇,不過這些影片我都還沒看過。說真的「砂之器」的故事,並沒有很特別,也許是因為我看了太多科幻與奇幻的故事,寫實的小說反而沒引起我的興趣。

但畢竟這本書是1960就開始連載的作品,我都還沒出生就問世的作品,也許把作品放在那個時代來看,這麼寫實地把兇殺、追查等過程寫出來,真的是個創舉。

關於作者松本清張,有個「清張革命」的說法,歷久彌新的「清張傳奇」——閱讀松本清張 裡面提到,推理小說是松本清張的工具,推理是為了探究犯罪的動機,鋪陳犯罪動機又是為了彰顯社會正義。透過小說,作者不希望日本人再將不愉快的記憶、難堪的狀況、痛苦的責任,全都堆到集體潛意識的黑暗角落裡去。

犯罪來自於人們表裡不一的性格,想推卸責任,冒充自己並不具備的高貴人格,正義的一方依著推理與好奇心,抽絲剝繭找出答案,這是作者希望日本人能找回來的推理心,該把黑暗角落的記憶找出來,面對它,解決它。

「砂之器」也就是「以砂子堆砌的容器」,象徵不實在的、隨時可能崩垮的東西,名利、權力、聲譽、地位等等東西,都可能變成不堪回首的記憶,無形的刀刃刻劃著自己的記憶。

以我現在對當時日本時代的粗淺認知,沒辦法再深入討論別的了。

日劇:砂の器(砂之器)
砂之器/砂の器 (2004/2011)~~史詩級的波瀾壯闊
砂之器
砂之器 wiki

DevOps (Development + Operations)

傳統的軟體開發,系統維運與品質保證三個單位各自獨立,因為部門之間的區隔,形成了 Wall of Confusion,為了讓整個開發活動在這幾個參與的單位之間,形成更有效率更有生產力的程序,轉化部門之間的對立為正向的互助合作,dev2ops.org 提出DevOps,希望能補足目前開發流程的不足。



上圖說明了系統開發與維運兩個單位之間的隔閡,因為單位不同,各單位傾向於維護自己單位的利益,系統維運為求系統穩定,通常會盡所有可能的方式,阻擋新版程式的更新,因為更新就有可能會帶來混亂,而不穩定的系統通常會歸咎於維運單位的問題,而如果到最後,又找出來問題的根源在於開發單位的錯誤、更新程序的說明文件不足、新版程式測試不完整,這種種可能的原因,最後上線了,都得要讓客服與維運人員承受客戶的壓力,惡性循環下,讓兩個單位之間的高牆更高、更難跨越。



業務單位的需求來自客戶,而開發單位需要更新程式,最常發生的原因是來自業務單位的需求,這也是業務跟開發之間的高牆,開發單位需要有個開發的程序,用來管理客戶需求跟開發的步驟。這個部份是目前討論最多的,也就是軟體工程的方法論,傳統的方式是 waterfall,後來進化到 Unified Processing,然後是 Agile Development Process。

DevOps將開發活動從客戶需求,實際的開發與測試,延伸到最後維運單位的系統更新與上線,DevOps 也可看成是開發、QA與維運三個單位的交集。



DevOps wiki 提供了一份很清楚的說明,DevOps 就是要解決系統發布與更新的問題,解決的方式就類似由 waterfall 轉變到 agile development 的過程,打破一個月、一季或更長時間的更新堆積,導入 DevOps 帶來的影響是

  1. 減少變更範圍:每天或每週更新一次,減少更新帶來的變化,更新的速度更頻繁,讓系統開發與維運的活動更熱絡,養成工作習慣的 routine,少量更新也讓所有人員更容易找出發生問題的地方
  2. 加強發布協調:以發布協調人員居中協調系統開發與維運之間的橋樑,採用各種協調互動的工具,讓所有人了解系統更新的程序與內容
  3. 自動化:自動化部署程序,確認可重複性的系統更新,減少出錯的可能

其實 DevOps 就等同於 Release Management,但是更強調快速與自動化的部署與驗證,還有開發跟維運兩個單位之間的互助合作。

身為開發人員,我們接受 agile development 的概念,貼近客戶與業務的需求,擁抱改變,還要以 DevOps 補足開發與維運之間的落差,認清系統的更新變化是常態的事實,既然不能拒絕改變,那麼就要擁抱改變,習慣它,然後持續尋求更好的方法與變化共處。