Erlang學習筆記(五)記錄與對映組

畫船聽雨發表於2017-08-28

記錄其實就是元組的另一種形式。通過使用記錄,可以給元組裡的各個元素關聯一個名稱。
對映組是鍵-值的關聯性集合。鍵可以是任意的Erlang資料型別。

1. 何時使用對映組或記錄

使用記錄的情況:
1. 當你可以用一些預定且數量固定的原子來表示資料時;
2. 當記錄裡的元素數量和元素名稱不會隨著時間而改變時;
3. 當儲存空間是個問題時,典型的案例是你有一大堆元組,並且沒一個元組都有相同的結構。
使用對映的情況:
1. 當鍵不能預先知道時用來表示鍵-值資料結構;
2. 當存在大量不同的鍵時用來表示資料;
3. 當方便使用很重要而效率無關緊要時作為萬能的資料結構實現;
4. 用作“自解釋型”的資料結構,也就是說,使用者容易從鍵名猜出鍵值的含義;
5. 用來表示鍵-值解析樹,例如XML或配置檔案;
6. 用Json來和其他程式語言通訊。

2. 通過記錄命名元組裡的項

一旦命名了元組裡的元素,就可以通過名稱來指向他們,而不必記住它們在元組裡面的具體位置。

-record(Name,{
              %%以下兩個鍵帶有預設值
              key1 = Default1,
              key2 = Default2,
              ……
              %%下一行就相當於key 3 = undefined
              key3,
              ……
                }).

舉個栗子,假設想要操作一個待辦項列表。我們首先定義一個todo記錄,然後將它儲存在一個檔案裡(記錄的定義既可以儲存在Erlang原始碼檔案裡,也可以由副檔名為.hrl的檔案儲存,然後包含在原始碼檔案裡)。
檔案包含是唯一能確保多個Erlang模組共享記錄定義的方式。類似於C語言中的.h檔案儲存公眾定義,然後包含在原始碼檔案裡。

-record(todo, {status=reminder, who = joe, text}).

記錄一旦被定義,就可以建立記錄的例項了。
在Shell中,必須先把記錄的定義讀入shell,然後才能建立記錄。我們將用shell函式rr(read records 的縮寫,即讀取記錄)來實現。

rr("records.hrl").

2.1 建立和更新記錄

建立新的記錄:

#todo{}.
X1 = #todo{status=urgent, text="Fix errata in book"}.

複製一個現有的記錄

X2 = X1#todo{status=done}.

X2是建立了X1的一個副本(型別必須是todo),並修改欄位status的值為done。請記住生成的是原始記錄的一個副本,原始記錄並沒有變化。

2.2 提取記錄欄位

在一次操作中提取記錄的多個欄位,可以使用模式匹配。

#todo{who = W, text = Txt} = X2.
%todo{status = done, who = joe, text = "Fix errata in book"}
W.
%joe
TXt
%"Fix errata in book"

在(=)的左側編寫一個記錄模式,如果匹配成功變數就會繫結記錄裡的相應欄位。
如果只是想要記錄裡的單個欄位,就可以使用“點語法”來提取該欄位。

X2#todo.text.
%"Fix errata in book"

2.3 在函式裡模式匹配記錄

編寫模式匹配記錄欄位或者建立新記錄的函式,程式碼如下:

clear_status(#todo{status = S, who = W} = R) ->
    %在此函式的內部,S和W繫結了記錄裡的欄位
    %R是*整個*記錄
    R#todo{status=finished}
    %%……

要匹配某個型別的記錄,可以這樣編寫函式的定義:

do_something(X) when is_record(X, todo) ->
    %%……

此時會在X是todo型別的記錄時匹配成功。

2.4 記錄是元組的另一種形式

X2.
%todo{status = done, who = joe,text = "Fix errata in book"}

在Shell中忘記todo記錄的定義

rf(todo).
%ok
X2.
%{todo,done,joe,"Fix errata in book"}

3. 對映組

對映組從Erlang的R17版開始使用。
對映組有以下屬性。
1. 對映組的語法與記錄相似,不同之處是省略了記錄名,並且鍵-值分隔符是=>或:=。
2. 對映組是鍵-值對的關聯集合。
3. 對映組裡的鍵可以是完全繫結的Erlang資料型別(即資料結構裡沒有任何未繫結變數)。
4. 對映組裡的各個元素根據鍵進行排序。
5. 在不改變鍵的情況下更新對映組是一種節省空間的操作。
6. 查詢對映組裡某個鍵的值是一種高效的操作。
7. 對映組有明確的順序。

3.1 對映組語法

對映組的語法如下:

#{Key1 Op Val1, Key2 Op Val2, .... , KeyN Op ValN}

它的語法與記錄相似,但是雜湊符號(即#)之後沒有記錄名,而Op是=>或者:=這兩個符號的其中一個。
舉例:

F1 = #{a => 1, b =>2}.
%#{a => 1, b => 2}.
Facts = #{ {wife, fred} = "Sue", {age, fred} => 45,
        {daughter,fred} => "Mary",
        {likes, jim} => [...]}
%#{{age,fred} => 45, {daughter,fred}=>"Mary", ...}

對映組在系統內部是作為有序集合儲存的,列印時總是使用個鍵排序後的順序,與對映組的建立方式無關。
舉例如下:

F2 = #{b => 2, a => 1}.
%#{a => 1, b => 2}.

基於現有的對映組更新一個對映組,我們會使用如下語法,其中的Op(更新操作)是=>或:=

NewMap = OldMap#{K1 Op V1, ...., Kn Op Vn}

表示式k=>V有兩種用途,一種是將現有鍵K的值更新為新值V,另一種是給對映組新增一個全新的K-V對。這個操作總是會成功的。
表示式K := V的作用是將現有鍵K的值更新為新值V。如果被更新的對映組不包含鍵值K,這個操作就會失敗。

使用:=有兩個重要的原因:
1. 拼錯了=鍵名的時候可以進行提醒;
2. 如果在更新操作中只使用:=操作符,那麼我們就知道新舊對映組都帶有一組相同的鍵,因此可以共享相同的鍵描述。

使用對映組最佳的方式是:首次定義某個鍵時總是使用Key = Val,而在修改操作時都使用Key := Val。

3.2 模式匹配對映組欄位

用來更新對映組的:=語法還可以作為對映組模式使用。和之前一樣,對映組模式裡的鍵不能包含任何未繫結的變數,但是現在的值可以包含未繫結的變數了(在模式匹配成功後繫結)。

Henry8 = #{class => King, born => 1491, died => 1547}.
%#{born => 1491, class=> King, died => 1547}.
#{born := B} = Henry8.
%#{born => 1491, class=> King, died => 1547}.
B.
%1494
#{D => 1547}
%variable 'D' unbound

可以在函式的頭部使用包含模式的對映組,前提是對映組裡所有的鍵都是已知的。
例子如下:

%統計字串中各個字元出現的次數
-module(test_count_characters).
-export([count_characters/1]).
 count_characters(Str) ->
    count_characters(Str,#{}).

count_characters([H|T], #{H := N} = X) ->
    count_characters(T, X#{H := N+1});
count_characters([H|T], X) ->
    count_characters(T, X#{H => 1});
count_characters([],X) -> 
X.  

3.3 操作對映組的內建函式

maps:new() -> #{} %返回一個空的對映組
erlang:is_map(M)  %如果M是對映組返回true否則返回false。可以用在關卡測試或函式主體中。
map:to_list(M) -> [{K1, V1}, ... ,{Kn, Vn}] %把對映組M裡的所有鍵和值轉換成一個鍵值列表,鍵值在生成的列表裡嚴格按照升序排列。
maps:from_list([{K1, V1}, ...., {Kn, Vn}]) -> M %把一個包含鍵值對的列表轉換成一個對映組M。如果同樣的鍵不止一次的出現,就使用列表裡第一鍵所關聯的值,後續的值都會被忽略。
maps:size(Map) -> numberOfEntries %返回對映組裡的條目數量。
maps:is_key(Key, Map) -> boolean()%如果對映組包含一個鍵未key的項就返回true,否則返回false。
maps:get(Key, Map) -> val %返回對映組裡與Key關聯的值,否則丟擲一個異常錯誤。
maps:find(Key, Map) -> {ok, Value} | error。%返回對映組與Key關聯的值,否則返回error。
maps:keys(Map) -> [Key1, ..., KeyN] %返回對映組所含的鍵列表,按升序排序
maps:remove(Key, M) -> M1%返回一個新對映組M1,除了鍵未Key的項(如果有的話)被移除外,其他與M一致。
maps:without([Key1, ..., KeyN], M) -> M1 %返回一個新對映組M1,它是M的複製,但移除了帶有[Key1,..., KeyN]列表裡這些鍵的元素。
maps:difference(M1, M2) -> M3 %M3是M1的複製,但移除了那些與M2裡的元素具有相同鍵的元素
%他的行為類似於下面的定義
maps:difference(M1, M2) ->
    maps:without(maps:keys(M2), M1).

3.4 對映組排序

對映組在比較時,首先會比大小,然後再按照鍵的排序比較鍵和值。

3.5 以JSON為橋樑

JSON與Erlang值的對映關係如下:
1. JSON的數字用Erlang的整數或浮點數表示。
2. JSON的字串用Erlang的二進位制。
3. JSON的列表用Erlang的列表。
4. JSON的true和false用Erlang的原子true和false表示。
5. JSON的物件用Erlang的對映組表示,但是有限制:對映組裡的鍵必須是原子、字串或者二進位制,而值必須可以用JSON的資料型別表示。

4. 小結

可以類比C/C++語言等的對映關係,等等來理解與思考記錄與對映組,並加入函數語言程式設計的思維,先巨集觀上的瞭解,然後再一點點去實驗,希望儘快掌握。

相關文章