30 分鐘學 Erlang (一)
本文寫給誰看的?
那些已經有過至少一門程式語言基礎,並且需要快速瞭解Erlang,掌握其基本要點,並馬上投入工作中的人。
文章挺長,所以分成了幾篇。但只要掌握了本文中提到的這些知識點,你就可以上手工作了。剩下的就是在實踐中繼續學習。Erlang 讀作 ai lan, er lan, er lang 都行,但你別單個字母讀 E-R-L-A-N-G,那樣我就不跟你玩了。
什麼時候用 Erlang?
Erlang 的設計目標非常明確,就是專門為大型的電信系統設計。
所以它的應用場景和設計需求就是電信領域裡需要解決的問題。
主要是三個: 高併發、高容錯、軟實時。電信系統負載非常大,需要同時服務大量使用者的能力;同時不允許出錯,電話頻繁掉線會很快把客戶趕到競爭對手那邊;再者,即便某個通話再繁忙也不能影響其他通話的正常進行,到技術層面就是,不能因為某個任務很重,就把其他的任務的資源都佔用了,while loop 佔用 100% CPU是絕對不允許的。
Erlang 是實用主義的語言,目的是 "get things done",它所有的特性都是為這個目標服務。Erlang 早已經脫離電信行業,飛奔到網際網路行業了,因為這些年網際網路行業所面臨的問題,跟幾十年前的電信系統越來越像。如今,Erlang 正在進入物聯網行業,它將為世界物聯網的發展做出自己的貢獻。
開始吧
學習 Erlang 等小眾語言的過程中,沒有太多中文資料,所以這篇文章裡,對於名詞、概念類的,還是用英文原詞不做翻譯。以免造成以後學習的障礙。
安裝和使用
安裝
- For Homebrew on OS X: brew install erlang
- For MacPorts on OS X: port install erlang
- For Ubuntu and Debian: apt-get install erlang
- For Fedora: yum install erlang
- For FreeBSD: pkg install erlang
啟動 Erlang Shell
安裝完成後,在終端裡敲 'erl' 進入 Erlang 的 REPL,erlang shell:
➜ ~ erl
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Eshell V9.3 (abort with ^G)
1> io:format("hello world!~n").
hello world!
ok
2> 1 + 1.
2
3> q().
ok
➜ ~
上面 Erlang shell 裡:
- 第一行
io:format("hello world!~n").
向標準輸出寫了一行"hello world!"
, 並以 "~n" 換行結尾。最後顯示的那個ok
是io:format()
函式的返回值。 - 第二行
1 + 1.
做了個加法運算,返回值是2
。 - 第三行
q().
是退出 erlang shell, 是init:stop().
的快捷方式. 連續按兩次Ctrl - C
, 或者Ctrl - C
之後選q
, 是一樣的效果。
上面的兩個例子展示了幾個要點:
- Erlang 的每個語句都要用
.
結尾。 - Erlang 是函式式語言,所有的函式、表示式都必須有一個返回值。輸出 "hello world" 會返回一個 atom 型別的
ok
。 -
Ctrl - C
然後q
ora
, 或者q().
會退出 shell,如果你執行了程式碼的話,你的應用程式會連帶著一起關掉。所以線上系統千萬不要Ctrl - C
。
註釋
Erlang 裡用 % 來做行註釋,相當於C語言裡的 //, 或者Python裡的 #。 沒有塊註釋。
% I am a comment
test_fuc() ->
"test".
基本型別
摘取自 learn-you-some-erlang,併為你們這些有經驗的程式設計師刪減和加工
Numbers
1> 2 + 15.
17
2> 49 * 100.
4900
3> 1892 - 1472.
420
4> 5 / 2. %% 最常用的浮點數除法
2.5
5> 5 div 2. %% div 是整除
2
6> 5 rem 2. %% rem 是取餘運算
1
...
%% 數字前面可以用 ‘#’ 來標註其 ‘Base’
%% 語法:Base#Value
%% 預設的 Base 是 10
...
10> 2#101010. %% 2 進位制的 101010
42
11> 8#0677. %% 8 進位制的 0677
447
12> 16#AE. %% 16 進位制的 AE
174
變數
Erlang 是函式式語言(雖然也支援副作用)。這意味著 Erlang 裡的變數 ‘ Immutable’ (不可變的).
Immutable variables 在設計上簡單,減少了併發過程中處理狀態改變帶來的複雜性。理解這一點很重要。
Erlang 是動態型別的語言,但它也是強型別的語言。動態型別意味著你宣告變數時不需要指定型別,而強型別是說,erlang 不會偷偷做型別轉換:
1> 6 + "1".
** exception error: bad argument in an arithmetic expression
in operator +/2
called as 6 + "1"
Erlang 裡變數的命名有約定,必須首字母大寫。因為首字母小寫的,會被認為是 atom
(原子) 型別。這一點在 elixir 裡有改進
正常的變數命名比如 Hello, Test. 而像 hello, test 這種的不是變數名,他們是 atom
型別,跟數字、字串一樣,是值型別:
1> Hello = "hello?".
"hello?"
2> Test = "testing words".
"testing words"
3> hello.
hello
4> V1 = hello. %% bind atom hello to V1
hello
5> V1.
hello
Erlang 裡沒有賦值語句。=
在 Erlang 裡是 pattern matching
(匹配、模式匹配),如果 =
左側跟右側的值不相等,就叫沒匹配上,這時那個 erlang 程式會直接異常崩潰(不要害怕,erlang 裡面崩潰挺正常的)。如果 =
左側的變數還沒有值,這次匹配過後,右側的值就會 bind
(繫結) 到那個變數上。
1> One. %% 變數沒繫結,不能使用。所以這裡出錯了。
* 1: variable 'One' is unbound
2> One = 1. %% 匹配變數 One 與 1. 由於One 之前沒有繫結過值,這裡將 Number 1 繫結給 One
1
3> Un = Uno = One = 1.
%% 1) 匹配 Un, Uno, One 和 1. One 的值是 1, 所以最右側的 One = 1 匹配成功,匹配操作返回值是 1.
%% 2) 然後繼續與左邊的 Uno 匹配。 Uno 之前沒有繫結過值,所以將 1 繫結給 Uno,匹配操作返回值也是 1.
%% 3) 同理 Un 也被繫結為 1. 返回值也是 1.
1
4> Two = One + One. %% Two 這時候被繫結為 2.
2
5> Two = 2. %% 嘗試匹配 2 = 2. 成功並返回 2.
2
6> Two = Two + 1. %% 嘗試匹配 2 = 3. 失敗了,所以當前的 erlang shell 程式崩潰了,然後又自動給你啟動了一個新的 erlang shell。
** exception error: no match of right hand side value 3
7> two = 2. %% 嘗試匹配一個 atom 和一個數字: two = 2. 匹配, 失敗崩潰了。
** exception error: no match of right hand side value 2
8> _ = 14+3. %% 下劃線 _ 是個特殊保留字,表示 "ignore",可以匹配任何值。
17
9> _.
* 1: variable '_' is unbound
10> _Ignore = 2. %% 以下劃線開頭的變數跟普通的變數作用沒有什麼區別,只不過在程式碼中,以下滑線開頭的變數告訴編譯器,"如果這個變數後面我沒用到的話,也不要警告我!"
2
11> _Ignore.
2
12> _Ignore = 3.
** exception error: no match of right hand side value 3
Atoms
上面已經提到過了,Erlang 裡面有 atom
型別,atom 型別使用的記憶體很小,所以常用來做函式的引數和返回值。參加 pattern matching 的時候,運算也非常快速。
在其他沒有 atom 的語言裡,你可能用過 constant
之類的東西,一個常量需要對應一個數字值或者其他型別的值。比如:
const int red = 1;
const int green = 2;
const int blue = 3;
但多了這個對映,其實用起來不大方便,後面對應的值 1, 2,3 一般只是用來比較,具體是什麼值都關係不大。所以有了 atom
就很方便了,我們從字面上就能看出,這個值是幹嘛的:
1> red.
red
atom
型別支援的寫法:
1> atom.
atom
2> atoms_rule.
atoms_rule
3> atoms_rule@erlang.
atoms_rule@erlang
4> 'Atoms can be cheated!'. %% 包含空格等特殊字元的 atom 需要用單引號括起來
'Atoms can be cheated!'
5> atom = 'atom'.
atom
需要注意的是:在一個 erlang vm 裡,可建立的 atom 的數量是有限制的(預設是 1,048,576 ),因為erlang 虛擬機器建立 atom 表也是需要記憶體的。一旦建立了某個 atom,它就一直存在那裡了,不會被垃圾回收。不要在程式碼裡動態的做 string -> atom 的型別轉換,這樣最終會使你的 erlang atom 爆表。比如在你的介面邏輯處理的部分做 to atom 的轉換的話,別人只需要用不一樣的引數不停地呼叫你的介面,就可以攻擊你。
Boolean 以及比較
atom
型別的 true
和 false
兩個值,被用作布林處理。
1> true and false. %% 邏輯 並
false
2> false or true. %% 邏輯 或
true
3> true xor false. %% 邏輯 異或
true
4> not false. %% 邏輯 非
true
5> not (true and true).
false
還有兩個與 and
和 or
類似的操作:andalso
和 orelse
。區別是 and
和 or
不論左邊的運算結果是真還是假,都會執行右邊的操作。而 andalso
和 orelse
是短路的,意味著右邊的運算不一定會執行。
來看一下比較:
6> 5 =:= 5. %% =:= 是"嚴格相等"運算子,== "是大概相等"
true
7> 1 =:= 0.
false
8> 1 =/= 0. %% =/= 是"嚴格不等"運算子,/= "是相差很多"
true
9> 5 =:= 5.0.
false
10> 5 == 5.0.
true
11> 5 /= 5.0.
false
一般如果懶得糾結太多,用 =:= 和 =/= 就可以了。
12> 1 < 2.
true
13> 1 < 1.
false
14> 1 >= 1. %% 大於等於
true
15> 1 =< 1. %% 注意這個 "小於等於" 的寫法,= 在前面。因為 => 還有其他的用處。。
true
17> 0 == false. %% 數字和 atom 型別是不相等的
false
18> 1 < false.
true
雖然不同的型別之間可以比較,也有個對應的順序,但一般情況用不到的:number < atom < reference < fun < port < pid < tuple < list < bit string
Tuples
Tuple
型別是多個不同型別的值組合成的型別。有點類似於 C 語言裡的 struct
。
語法是:{Element1, Element2, ..., ElementN}
1> X = 10, Y = 4.
4
2> Point = {X,Y}. %% Point 是個 Tuple 型別,包含了兩個整形的變數 X 和 Y
{10,4}
實踐中,我們經常 在 tuple 的第一個值放一個 atom 型別,來標註這個 tuple 的含義。這種叫做 tagged tuple:
1> Data1 = {point, 1, 2}.
{point,1,2}
2> Data2 = {rectangle, 20, 30}.
{rectangle,20,30}
後面的程式碼如果要處理 Data1 和 Data2 的話,只需要檢查 tuple 的第一項,就知道這個 tuple 是個點座標,還是個矩形:
3> case Data1 of
3> {point, X, Y} -> "this is a point";
3> {rectangle, Length, Width} -> "this is a rectangle"
3> end.
"this is a point"
上面用 case
做 pattern matching ,這個後面還要講。
List
List
就是我們經常說的連結串列,資料結構裡學的那個。但 List 型別在 Erlang 裡使用極其頻繁,因為用起來很方便。
List
可以包含各種型別的值:
1> [1, 2, 3, {numbers,[4,5,6]}, 5.34, atom].
[1,2,3,{numbers,[4,5,6]},5.34,atom]
上面這個 list 包含了數字型別 1,2,3,一個 tuple,一個浮點數,一個 atom 型別。
來看看這個:
2> [97, 98, 99].
"abc"
臥槽這什麼意思?!因為 Erlang 的 String 型別其實就是 List!所以 erlang shell 自動給你顯示出來了。
就是說如果你這麼寫 "abc"
, 跟 [97, 98, 99]
是等效的。
連結串列儲存空間還是比純字串陣列大的,拼接等操作也費時,所以一般如果你想用 '真 · 字串' 的時候,用 Erlang 的 Binary
型別,這樣寫:<<"abc">>
。這樣記憶體消耗就小很多了。Binary 這是後話了,這篇文章裡不介紹。
我知道一開始你可能不大明白 tuple 跟 list 的區別,這樣吧:
- 當你知道你的資料結構有多少項的時候,用
Tuple
; - 當你需要動態長度的資料結構時,用
List
。
List 處理:
5> [1,2,3] ++ [4,5]. %% ++ 運算子是往左邊的那個 List 尾部追加右邊的 List。
%% 這樣挺耗時的。連結串列嘛你知道的,往連結串列尾部追加,需要先遍歷這個連結串列,找到連結串列的尾部。
%% 所以 "abc" ++ "de" 這種的操作的複雜度,取決於前面 "abc" 的長度。
[1,2,3,4,5]
6> [1,2,3,4,5] -- [1,2,3]. %% -- 是移除操作符。
[4,5]
7> [2,4,2] -- [2,4].
[2]
8> [2,4,2] -- [2,4,2].
[]
9> [] -- [1, 3]. %% 如果左邊的 List 裡不包含需要移除的值,也沒事兒。不要拿這種東西來做面試題,這樣會沒朋友的。
[]
11> hd([1,2,3,4]).
1
12> tl([1,2,3,4]).
[2,3,4]
上面 hd/1 是取 Head 函式。tl/1 是取 Tail. 這倆都是 BIF (Built-In-Function),就是 Erlang 內建函式.
第一行裡你也看到了,List 的追加操作會有效能損耗 (lists:append/2 跟 ++ 是一回事兒),所以我們需要一個從頭部插入 List 的操作:
13> List = [2,3,4].
[2,3,4]
14> NewList = [1|List]. %% 注意這個 | 的左邊應該放元素,右邊應該放 List。
[1,2,3,4]
15> [1, 2 | [0]]. %% 左邊元素有好幾個的話,erlang 會幫你一個一個的插到頭部。先插 2,後插1.
[1,2,0]
16> [1, 2 | 0]. %% 右邊放的不是 List,這種叫 'improper list'。
%% 雖然你可以生成這種列表,但不要這麼做,程式碼裡出現這種一般就是個 bug。忘了這種用法吧。
[1,2|0]
20> [1 | []]. %% List 可以分解為 [ 第一個元素 | 剩下的 List ]。仔細看一下這幾行體會一下。
[1]
21> [2 | [1 | []]].
[2,1]
22> [3 | [2 | [1 | []] ] ].
[3,2,1]
List Comprehensions
實踐中我們經常會從一個 List 中,取出我們需要的那些元素,然後做處理,最後再將處理過的元素重新構造成一個新的元素。
你馬上就想到了 map,reduce。在 Erlang 裡,我們可以用 List Comprehensions 語法,很方便的做一些簡單的處理。
1> [2*N || N <- [1,2,3,4]]. %% 取出 [1,2,3,4] 中的每個元素,然後乘2,返回值再組成一個新的 List
[2,4,6,8]
2> [X || X <- [1,2,3,4,5,6,7,8,9,10], X rem 2 =:= 0]. %% 取出右邊列表裡所有偶數。
[2,4,6,8,10]
Anonymous functions
讓我們定義一個函式:
Add = fun (A, B) -> A + B end.
上面的程式碼裡,我們用 fun() 定義了一個 匿名函式, 接收兩個引數,並將兩個引數的和作為返回值。
最後將這個函式 bind 到 Add 變數:
1> Add = fun (A, B) -> A + B end.
#Fun<erl_eval.12.118419387>
2> Add(1, 2).
3
Modules
本章程式碼在:https://github.com/terry-xiaoyu/learn-erlang-in-30-mins/tree/master/modules
Erlang Shell 是一個快速嘗試新想法的地方,但我們真正的程式碼是要寫到檔案裡,然後參與編譯的。
Erlang 裡程式碼是用 Module 組織的。一個 Module 包含了一組功能相近的函式。
用一個函式的時候,要這麼呼叫:Module:Function(arg1, arg2)
。
或者你先 import
某個 Module 裡的函式,然後用省略Module名的方式呼叫:Function(arg1, arg2)
。
Module 可也提供程式碼管理的作用,載入一個 Module 到 Erlang VM就載入了那個 Module 裡的所有程式碼,然後你想熱更新程式碼的話,直接更新這個 Module 就行了。
來看 Erlang 自帶的幾個 Module:
1> erlang:element(2, {a,b,c}).
b
2> element(2, {a,b,c}).
b
3> lists:seq(1,4).
[1,2,3,4]
4> seq(1,4).
** exception error: undefined shell command seq/2
上面的例子裡,你能直接用 erlang
Module 裡的 element/2 函式,是因為 erlang 裡的常用函式會被 潛在的 import
過來。其他的 Module 比如 lists 不會.
erlang
module 裡的函式叫做 BIF
.
使用 Module 寫 functions:
建立一個名為 useless.erl 的檔案。
在檔案的第一行, 用 -module(useless) 來宣告你的 module name。注意跟 java 類似,module 名要跟檔名一樣。
然後你在你的 module 裡寫你的函式:
-module(useless).
-export([add/2, add/3]). %% export 是匯出語法,指定匯出 add/2, add/3 函式。沒匯出的函式在 Module 外是無法訪問的。
add(A, B) ->
A + B.
add(A, B, C) ->
A + B + C.
然後你用 erlc 編譯
mkdir -p ./ebin
erlc -o ebin useless.erl
編譯後的 beam 檔案會在 ebin 目錄下,然後你啟動 erlang shell:
$ erl -pa ./ebin
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3 (abort with ^G)
1> useless:add(1, 2).
3
2> useless:add(1, 2, 1).
4
erl -pa 引數的意思是 Path Add
, 新增目錄到 erlang 的 beam 檔案查詢目錄列表裡。
就是說,你執行 useless:add(1, 2). 的時候,erlang 發現 module 'useless' 沒載入,就在那些查詢目錄裡找 useless.beam,然後載入進來。
Erlang 裡面函式是用 函式名/引數個數來表示的,如果兩個函式的函式名與引數個數都一樣,他們就是一個函式的兩個分支,必須寫在一起,分支之間用分號分割。
上面的 add(A, B) 可以叫做 add/2, 而 add(A, B, C) 函式叫做 add/3. 注意這個 add/3和 add/2 因為引數個數不一樣,所以被認為兩個不同的函式,即使他們的函式名是一樣的。
所以,第一個函式用 .
結尾。如果是一個函式的多個 clause, 是要用 ;
分割的:
-module(clauses).
-export([add/2]).
%% goes into this clause when both A and B are numbers
add(A, B) when is_number(A), is_number(B) ->
A + B;
%% goes this clause when both A and B are lists
add(A, B) when is_list(A), is_list(B) ->
A ++ B.
%% crashes when no above clauses matched.
上面程式碼裡,定義了一個函式:add/2. 這個函式有兩個 clause 分支,一個是計算數字相加的,一個是計算字串相加的。
程式碼裡 when
是一個 Guard
關鍵字。Pattern Matching
和 Guard
後面講解。
執行 add/2 時會從上往下挨個匹配:
$ erl -pa ebin/
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3 (abort with ^G)
1> clauses:add("ABC", "DEF"). %% 第一個 clause 沒匹配上。走的是第二個 clause。
"ABCDEF"
2> clauses:add(1, 2). %% 走第一個 clause
3
3> clauses:add(1, 2.4).
3.4
4> clauses:add(1, "no"). %% 兩個 clause 都沒匹配上,崩潰了。
** exception error: no function clause matching clauses:add(1,"no") (clauses.erl, line 4)
常用知識點
Pattern Matching
Erlang 裡到處都用匹配的。
1. case clauses
下面的程式碼裡,我們定義了一個 greet/2 函式
-module(case_matching).
-export([greet/2]).
greet(Gender, Name) ->
case Gender of
male ->
io:format("Hello, Mr. ~s!~n", [Name]);
female ->
io:format("Hello, Mrs. ~s!~n", [Name]);
_ ->
io:format("Hello, ~s!~n", [Name])
end.
case 的各個分支是自上往下依次匹配的,如果 Gender 是 atom 'male', 則走第一個,如果是 'female' 走第二個,如果上面兩個都沒匹配上,則走第三個。
$ erl -pa ebin/
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3 (abort with ^G)
1> case_matching:greet(male, "Shawn").
Hello, Mr. Shawn!
ok
2. function clauses
我們把上面的例子改一下,讓程式碼更規整一點:
-module(function_matching).
-export([greet/2]).
greet(male, Name) ->
io:format("Hello, Mr. ~s!~n", [Name]);
greet(female, Name) ->
io:format("Hello, Mrs. ~s!~n", [Name]);
greet(_, Name) ->
io:format("Hello, ~s!~n", [Name]).
這個 function 有三個 clause,與 case 一樣,自上往下依次匹配。
$ erl -pa ebin/
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3 (abort with ^G)
1> function_matching:greet(female, "Scarlett").
Hello, Mrs. Scarlett!
ok
2>
在匹配中獲取值
3> {X, 1, 5} = {2, 1, 5}. %% 如果匹配成功的話,將對應的值 bind 到 X 上。
{2,1,5}
4> X.
2
5> [H | T] = [1, 2, 3]. %% 現在我們使用匹配來解析 List,將第一個元素繫結到 H, 將其餘繫結到 T。
[1,2,3]
6> H.
1
7> T.
[2,3]
8> [_ | T2] = T. %% 我可以一直這麼做下去
[2,3]
9> T2.
[3]
10> [_ | T3] = T2. %% 再來
[3]
11> T3.
[]
12> f(). %% Erlang 裡面變數是 immutable 的,所以我們現在解綁一下所有變數,清理之前用過的變數名。
ok
13> Add = fun({A, B}) -> A + B end. %% 我們重新定義了 Add 函式,現在它只接收一個 tuple 引數
%% 然後在引數列表裡我們做了 pattern matching 以獲取 tuple 中的兩個值,解析到 A,B.
#Fun<erl_eval.6.118419387>
14> Add({1, 2}).
3
好了,就問你厲不厲害?
Guards
前面有用過 when
, 提到過 guards. 現在我們來認真討論它:
learn-you-some-erlang 的作者那邊 16歲才能"開車" (笑). 那我們寫個函式判斷一下,某個人能不能開車?
old_enough(0) -> false;
old_enough(1) -> false;
old_enough(2) -> false;
...
old_enough(14) -> false;
old_enough(15) -> false;
old_enough(_) -> true.
上面這個又點太繁瑣了,所以我們得另想辦法:
old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.
然後作者又說了,超過 104 歲的人,禁止開車:
right_age(X) when X >= 16, X =< 104 -> %% 注意這裡用了逗號,表示 and
true;
right_age(_) ->
false.
when
語句裡,,
表示 and
, ;
表示 or
, 如果你想用短路運算子的話,用 andalso
和orelse
, 這麼寫:
right_age(X) when X >= 16 andalso X =< 104 -> true;
Records
前面講過 tagged tuple
,但它用起來還不夠方便,因為沒有個名字,也不好訪問其中的變數。
我們來定義一個好用點的 tagged tuple
,Erlang 裡就是record
:
-module(records).
-export([get_user_name/1,
get_user_phone/1]).
-record(user, {
name,
phone
}).
get_user_name(#user{name=Name}) ->
Name.
get_user_phone(#user{phone=Phone}) ->
Phone.
$ erl
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3 (abort with ^G)
1> c(records). %% 這是編譯 erlang 程式碼的另外一種辦法。c/1 編譯並載入 module。
{ok,records}
2> rr(records). %% 將 records module 中的所有 record 都載入到 erl shell 裡。
[user]
4> Shawn = #user{name = <<"Shawn">>, phone = <<"18253232321">>}.
#user{name = <<"Shawn">>,phone = <<"18253232321">>}
5> records:get_user_phone(Shawn).
<<"18253232321">>
6> records:get_user_name(Shawn).
<<"Shawn">>
%% record 其實就是個 tagged tuple, 第一個元素是 record 名字。
7> records:get_user_name({user, <<"Shawn">>, <<"18253232321">>}).
<<"Shawn">>
9> Shawn#user.name.
<<"Shawn">>
10> #user.name.
2
你看到 #user{}
其實只是一個第一個元素為 user
的 tagged tuple {user, name, phone}
, 而 #user.name 是這個 tuple 裡 name
欄位的位置號 2。注意: Erlang 裡面的位置、Index 等都是約定從 1 開始的。
Shawn#user.name 的意思是取 Shawn 裡的第 2 個元素。
遞迴
Erlang 是函式式語言,變數 immutable 的,所以沒有 while loop。因為不能讓你定義一個遞增的 counter 變數。
所以我們用遞迴來解決大多數問題。
先來一個計算 List 長度的函式:
len([]) -> 0; %% 空列表的長度是 0
len([_|T]) -> 1 + len(T) %% 列表的長度,是 1 + 剩餘列表的長度。
簡單吧?但是你知道的,這樣子如果要計算的 List 長度太長的話,呼叫棧就特別長,會吃盡記憶體。計算過程是這樣的:
len([1,2,3,4]) = len([1 | [2,3,4])
= 1 + len([2 | [3,4]])
= 1 + 1 + len([3 | [4]])
= 1 + 1 + 1 + len([4 | []])
= 1 + 1 + 1 + 1 + len([])
= 1 + 1 + 1 + 1 + 0
= 1 + 1 + 1 + 1
= 1 + 1 + 2
= 1 + 3
= 4
所以我們必須用 Tail Recursion
(尾遞迴) 來改寫一下:
len(L) -> len(L,0). %% 這其實只是給 len/2 的第二個引數設定了一個預設值 0.
len([], Acc) -> Acc; %% 所有的元素都讀完了
len([_|T], Acc) -> len(T,Acc+1). %% 讀一個元素,Acc 增1,然後計算剩下的 List 的長度。
尾遞迴就是,最後一個語句是呼叫自身的那種遞迴。Erlang 遇到這總遞迴的時候,不會再保留呼叫棧。這樣的遞迴相當於一個 while loop。
我們用 Acc 來記錄每次計算的結果,讀取一個元素 Acc 就增 1,一直到讀取完所有的元素。
第一個例子裡,第二個 clause 的最後一個呼叫是 1 + len(T)
,這不是尾遞迴。因為系統還要保留著呼叫棧,等其算出 len(T) 之後,再回來跟 1 做加法運算。只有 len(T,Acc+1).
這種才是。
尾遞迴與遞迴的區別:
有個比喻可以幫你理解他們的差異。
假設玩一個遊戲,你需要去收集散落了一路,並通向遠方的硬幣。
於是你一個一個的撿,一邊撿一邊往前走,但是你必須往地上撒些紙條做記號,因為不做記號你就忘了回來的路。於是你一路走,一路撿,一路撒紙條。等你撿到最後一個硬幣時,你開始沿著記號回來了,一路走,一路撿紙條(保護環境)。等回到出發點時,你把硬幣裝你包裡,把紙條扔進垃圾桶。
這就是非尾遞迴,紙條就是你的呼叫棧,是記憶體記錄。
下次再玩這個遊戲時,你學聰明瞭,你直接揹著包過去了,一路走,一路撿,一路往包裡塞。等到了終點時,最後一個硬幣進包了,任務完成了,你不回來了!
這就是尾遞迴,省去了呼叫棧的消耗。
書接下文:30 分鐘學 Erlang (二)
相關文章
- 30分鐘SQL指南SQL
- 1 分鐘學會 30 種程式語言
- 30分鐘入門MyBatisMyBatis
- 30分鐘精通React HooksReactHook
- 【grunt第一彈】30分鐘學會使用grunt打包前端程式碼前端
- numpy 基礎入門 - 30分鐘學會numpy
- 【譯】30 分鐘入門 TypescriptTypeScript
- 30分鐘快速瞭解webpackWeb
- 30分鐘理解GraphQL核心概念
- [譯] 用 30 分鐘建立一個網站的方式來學習 Bootstrap 4網站boot
- Docker虛擬化管理:30分鐘教你學會用DockerDocker
- 前置機器學習(五):30分鐘掌握常用Matplotlib用法機器學習
- 前置機器學習(三):30分鐘掌握常用NumPy用法機器學習
- 30 分鐘內瞭解 IEC 61850
- 30 分鐘快速入門 Docker 教程Docker
- 30 分鐘理解 CORB 是什麼ORB
- [譯] 30 分鐘 Python 爬蟲教程Python爬蟲
- 30分鐘徹底弄懂flex佈局Flex
- PLAN A:30 分鐘未付款取消訂單
- 如何設定一個嚴格30分鐘過期的SessionSession
- 哥倫比亞大學:研究發現每坐30分鐘步行5分鐘可以大大減輕久坐危害
- 一分鐘學習Markdown語法
- 一分鐘學會《模板方法模式》模式
- 連續假期不無聊,只要 30 分鐘就能學會如何架設一個網站!網站
- 30分鐘讓你掌握Git的黑魔法Git
- 30分鐘講清楚深度神經網路神經網路
- 30分鐘搭建你的靜態網站網站
- 一分鐘sed入門(一分鐘系列)
- [譯] Erlang 之禪第一部分
- 30分鐘帶你搞定Dokcer部署Kafka叢集Kafka
- 30分鐘實現小程式語音識別
- 25~30K的學員面試考題,10分鐘就寫完?面試
- 五分鐘學會Markdown
- 5分鐘學會 gRPCRPC
- 【譯】5分鐘學習 JS 一些小技巧JS
- 30分鐘通過Kong實現.NET閘道器
- 寶付:30分鐘理解Spark的基本原理Spark
- 30分鐘閒置伺服器建站(gitlab為例)伺服器Gitlab