Elixir 簡明筆記(十八) --- 數(shù)據(jù)結(jié)構(gòu)實(shí)戰(zhàn)

介紹了Elixir的基本數(shù)據(jù)類型和控制結(jié)構(gòu),可以寫一個(gè)小應(yīng)用來實(shí)戰(zhàn)一下。針對(duì)elixir的數(shù)據(jù)抽象進(jìn)行寫一個(gè)簡單的todo應(yīng)用。

todo的使用方式大概如下:

todo_list = TodoList.new |>
            TodoList.add_entry({2013, 12, 19}, "Dentist") |>
            TodoList.add_entry({2013, 12, 20}, "Shopping") |>
            TodoList.add_entry({2013, 12, 19}, "Movies")

以日期的tuple作為key,todo的內(nèi)容作為value。Todo模塊提供一個(gè)增加todo的函數(shù)。

初步實(shí)現(xiàn)

從上面的使用方法來看,TodoList模塊有一個(gè)new函數(shù),用來創(chuàng)建一個(gè)todo“實(shí)例”。而todo實(shí)際數(shù)據(jù)結(jié)構(gòu)非常合適哈希結(jié)構(gòu),這里選擇了HashDict。

defmodule TodoList do

    def new, do: HashDict.new

end

剩下就是實(shí)現(xiàn)增加todo的函數(shù)??梢允褂?code>HashDict.update/4函數(shù),可以實(shí)現(xiàn)改功能

defmodule TodoList do

    def new, do: HashDict.new

    def add_entry(todo_list, date, title) do
        HashDict.update(
            todo_list,
            date,
            [title],
            fn titles -> [title|titles] end
        )
    end
end

update函數(shù)提供四個(gè)參數(shù),第一個(gè)是要操作的hashdict,第二個(gè)是key,第三個(gè)是value,如果所傳的key對(duì)于的value不存在,就調(diào)用第四個(gè)lambda函數(shù)。匿名函數(shù)接收一個(gè)存在的value作為參數(shù),返回一個(gè)列表。使用iex todo_list.ex運(yùn)行:

iex(3)> todo_list = TodoList.new
#HashDict<[]>
iex(4)> todo_list = TodoList.add_entry(todo_list, {2016, 12, 19}, "Dentist")
#HashDict<[{{2016, 12, 19}, ["Dentist"]}]>
iex(5)> todo_list = TodoList.add_entry(todo_list, {2016, 12, 20}, "Shopping")
#HashDict<[{{2016, 12, 19}, ["Dentist"]}, {{2016, 12, 20}, ["Shopping"]}]>
iex(6)> todo_list = TodoList.add_entry(todo_list, {2016, 12, 19}, "Movies")
#HashDict<[{{2016, 12, 19}, ["Movies", "Dentist"]},
 {{2016, 12, 20}, ["Shopping"]}]>

因?yàn)閑lixir的數(shù)據(jù)是不可變的,因此一直在針對(duì)todo_list進(jìn)行重新綁定。

下面要實(shí)現(xiàn)的一個(gè)方法則是通過key(date元組)來獲取相應(yīng)的title內(nèi)容

defmodule TodoList do

    def new, do: HashDict.new

    def add_entry(todo_list, date, title) do
        HashDict.update(
            todo_list,
            date,
            [title],
            fn titles -> [title|titles] end
        )
    end

    def entries(date) do
        HashDict.get(todo_list, date [])
    end
end

HashDict.get/3函數(shù)可以通過key讀取value,當(dāng)然也可以在value不存在的時(shí)候返回一個(gè)默認(rèn)的值。

抽象封裝

上述的實(shí)現(xiàn)完全可以work。可是還可以針對(duì)HashDict做出更高級(jí)的抽象。然后讓客戶端的代碼看起來可讀性更高。實(shí)現(xiàn)一個(gè)針對(duì)key和value的函數(shù)的模塊:

defmodule MultiDict do

    def new, do: HashDict.new

    def add(dict, key, value) do
        HashDict.update(
            dict,
            key,
            [value],
            &([value|&1])
        )
    end

    def get(dict, key) do
        HashDict.get(dict, key, [])
    end
end

通過抽象的MultiDict模塊可以重寫TodoList模塊

defmodule TodoList do

    def new, do: MultiDict.new

    def add_entry(todo_list, date, title) do
        MultiDict.add(todo_list, date, title)
    end

    def entries(date) do
        MultiDict.get(todo_list, date)
    end
end

使用map結(jié)構(gòu)

目前為止,經(jīng)過簡單的抽象,已經(jīng)讓Todo的客戶端代碼變得簡潔??墒窃谡{(diào)用的時(shí)候,key傳一個(gè)tuple還是讓閱讀性降低,既然todo是哈稀結(jié)構(gòu),那么參數(shù)也可以傳一個(gè)哈稀結(jié)構(gòu)就非常匹配。因此可以使用map來當(dāng)成todo的值來傳遞。

defmodule TodoList do
    
    def new, do: MultiDict.new

    def add_entry(todo_list, entry) do
        MultiDict.add(todo_list, entry.title, entry.value)
    end

    def entries(todo_list, title) do
        MultiDict.get(todo_list, title)
    end

end

iex(1)> entry1 = %{title: {2013, 12, 19}, value: "Dentist"}
%{title: {2013, 12, 19}, value: "Dentist"}
iex(2)> entry2 = %{title: {2013, 12, 20}, value: "Shopping"}
%{title: {2013, 12, 20}, value: "Shopping"}
iex(3)> entry3 = %{title: {2013, 12, 19}, value: "Movies"}
%{title: {2013, 12, 19}, value: "Movies"}
iex(4)>
nil
iex(5)> todo_list =
...(5)>           TodoList.new |>
...(5)>             TodoList.add_entry(entry1) |>
...(5)>             TodoList.add_entry(entry2) |>
...(5)>             TodoList.add_entry(entry3)
#HashDict<[{{2013, 12, 20}, ["Shopping"]},
 {{2013, 12, 19}, ["Movies", "Dentist"]}]>
iex(6)> TodoList.entries(todo_list, entry1.title)
["Movies", "Dentist"]

自增id的todo

前面我們實(shí)現(xiàn)了C和R兩個(gè)操作,接下來將會(huì)實(shí)現(xiàn)todo應(yīng)用的修改和刪除條目操作。通常而言,一個(gè)item條目,擁有一個(gè)id,這樣對(duì)這個(gè)條目的操作可以借助id來做 關(guān)系的處理。下面對(duì)todo進(jìn)行修改,客戶端的代碼還是一致,通過entry的title和value來創(chuàng)建todo,每一個(gè)條目的id都是自增的。這里使用了elixir的一種新的數(shù)據(jù)協(xié)議,struct。重寫CR功能。

defmodule TodoList do

    defstruct auto_id: 1, entries: HashDict.new 
    
    def new, do: %TodoList{}

    def add_entry(%TodoList{entries: entries, auto_id: auto_id} = todo_list, entry) do
        
        new_entry = Map.put(entry, :id, auto_id)
        new_entries = HashDict.put(entries, auto_id, new_entry)
        new_id = auto_id + 1
        
        %TodoList{todo_list | auto_id: new_id, entries: new_entries}

    end


    def entries(%TodoList{entries: entries}, date) do
        
        entries 
            |> Stream.filter(fn {_, entry} ->  entry.date == date end)
            |> Enum.map(fn {_, entry} -> entry end)
    end
end

上面的代碼,定義了一個(gè)struct,包含兩個(gè)字段,一個(gè)是自增的當(dāng)前id,默認(rèn)為1。另外這是todo的條目,默認(rèn)是一個(gè)空的HashDict。TodoList.new/0 方面很簡單,初始化一個(gè)todo模塊的實(shí)例。

TodoList.add_entry/2 是增加一個(gè)條目,第一個(gè)參數(shù)使用了模式匹配,將傳入的todo實(shí)例進(jìn)行模式匹配,第二個(gè)參數(shù)是用來增加的條目。新增的條目是一個(gè)map,因此使用put函數(shù)增加一個(gè)key為id,id的值為當(dāng)前自增的id,entries是一個(gè)HashDict。它的key都是自增id,值都在具體的條目,因此使用put函數(shù)新建一個(gè)new_entries。然后需要自增id,最后再使用struct的更新語法更新struct。因?yàn)樗碌膎ew_id以及新entriesHashDict。對(duì)于已經(jīng)存在的字段,可以使用|語法更新。

最后的 TodoList.entires/2 函數(shù)的第一個(gè)參數(shù)也有模式匹配,因?yàn)楹瘮?shù)內(nèi)不需要使用todo_list,因此可以省略而不用寫成%TodoList{entries: entries}=todo_list。具體邏輯則通過Stream模塊進(jìn)行迭代過濾,找出date與參數(shù)date相同的entry,然后再通過Enum的枚舉把最后的entry列表返回。

iex(1)> todo_list = TodoList.new |>
...(1)>           TodoList.add_entry(
...(1)>               %{date: {2013, 12, 19}, title: "Dentist"}
...(1)>           ) |>
...(1)>           TodoList.add_entry(
...(1)>             %{date: {2013, 12, 20}, title: "Shopping"}
...(1)> ) |>
...(1)>           TodoList.add_entry(
...(1)>             %{date: {2013, 12, 19}, title: "Movies"}
...(1)> )
%TodoList{auto_id: 4,
 entries: #HashDict<[{2, %{date: {2013, 12, 20}, id: 2, title: "Shopping"}},
  {3, %{date: {2013, 12, 19}, id: 3, title: "Movies"}},
  {1, %{date: {2013, 12, 19}, id: 1, title: "Dentist"}}]>}
iex(2)> TodoList.entries(todo_list, {2013, 12, 19})
[%{date: {2013, 12, 19}, id: 3, title: "Movies"},
 %{date: {2013, 12, 19}, id: 1, title: "Dentist"}]

使用自增id的方式,重寫了todo的CR更能,下一個(gè)功能則是下面

todo條目的更新和刪除

下面實(shí)現(xiàn)更新和刪除的功能。可以使用HashDict.update來更新一個(gè)HashDict。

defmodule TodoList do

    defstruct auto_id: 1, entries: HashDict.new 
    
    def new, do: %TodoList{}

    def add_entry(%TodoList{entries: entries, auto_id: auto_id} = todo_list, entry) do
        
        new_entry = Map.put(entry, :id, auto_id)
        new_entries = HashDict.put(entries, auto_id, new_entry)
        new_id = auto_id + 1
        
        %TodoList{todo_list | auto_id: new_id, entries: new_entries}

    end


    def entries(%TodoList{entries: entries}, date) do
        
        entries 
            |> Stream.filter(fn {_, entry} ->  entry.date == date end)
            |> Enum.map(fn {_, entry} -> entry end)
    end

    def update_entry(%TodoList{entries: entries}=todo_list, entry_id, unpdate_fun) do
        case entries[entry_id] do
            nil -> todo_list

            old_entry -> new_entry = unpdate_fun.(old_entry)
                         new_entries = HashDict.put(entries, new_entry.id, new_entry)
                         %TodoList{todo_list | entries: new_entries}    
        end
    end
end

iex(1)> todo_list = TodoList.new |>
...(1)>           TodoList.add_entry(
...(1)>               %{date: {2013, 12, 19}, title: "Dentist"}
...(1)>           ) |>
...(1)>           TodoList.add_entry(
...(1)>             %{date: {2013, 12, 20}, title: "Shopping"}
...(1)> ) |>
...(1)>           TodoList.add_entry(
...(1)>             %{date: {2013, 12, 19}, title: "Movies"}
...(1)> )
%TodoList{auto_id: 4,
 entries: #HashDict<[{2, %{date: {2013, 12, 20}, id: 2, title: "Shopping"}},
  {3, %{date: {2013, 12, 19}, id: 3, title: "Movies"}},
  {1, %{date: {2013, 12, 19}, id: 1, title: "Dentist"}}]>}
iex(3)> TodoList.entries(todo_list, {2013, 12, 20})
[%{date: {2013, 12, 20}, id: 2, title: "Shopping"}]
iex(8)> todo_list = TodoList.update_entry(
...(8)>           todo_list,
...(8)> 1,
...(8)>           &Map.put(&1, :date, {2013, 12, 20})
...(8)>         )
%TodoList{auto_id: 4,
 entries: #HashDict<[{2, %{date: {2013, 12, 20}, id: 2, title: "Shopping"}},
  {3, %{date: {2013, 12, 19}, id: 3, title: "Movies"}},
  {1, %{date: {2013, 12, 20}, id: 1, title: "Dentist"}}]>}
iex(10)> TodoList.entries(todo_list, {2013, 12, 20})
[%{date: {2013, 12, 20}, id: 2, title: "Shopping"},
 %{date: {2013, 12, 20}, id: 1, title: "Dentist"}]

更新的方式也是通過模式匹配。并且使用了case宏,如果是常規(guī)的編程語言,大概思路可能如下:

old_entry = Map.get(entries, entry_id, [])
if old_entry == [] do
    todo_list
else
    new_entry = unpdate_fun.(old_entry)
    new_entries = HashDict.put(entries, new_entry.id, new_entry)
    %TodoList{todo_list | entries: new_entries}     
end

實(shí)現(xiàn)delete方法很簡單,調(diào)用HashDict.delete/2 方法即可:

defmodule TodoList do
    ...

    def delete_entry(%TodoList{entries: entries}=todo_list, entry_id) do
        case entries[entry_id] do

            nil -> todo_list

            old_entry -> IO.puts inspect old_entry
                         new_entries = HashDict.delete(entries, entry_id)
                         %TodoList{todo_list | entries: new_entries}    
        end
    end

end

總結(jié)

Elixir提供的數(shù)據(jù)類型比較豐富,并且發(fā)展也很快,隨著Erlang的進(jìn)化,elixir也在不斷的跟進(jìn)。之前剛查詢HashDict的一些函數(shù),請(qǐng)教了一個(gè)朋友,他說,為啥不用map。原來最新的1.2.4版本map不象之前1.0版本那樣性能不足以支持大數(shù)據(jù)。最新的map已經(jīng)對(duì)多item的性能進(jìn)行了優(yōu)化,map可以取代HashDict。

無論HashDict還是Map。這些基本結(jié)構(gòu)的操作都少不了常規(guī)的方法,具體選取可以跟進(jìn)實(shí)際應(yīng)用場景結(jié)合最新的文檔。所謂的常規(guī)方法免不了需要進(jìn)行迭代。我們知道遞歸可以循環(huán),elixir還提供了一些高級(jí)函數(shù)封裝隱藏了這些迭代細(xì)節(jié)。下面將會(huì)介紹強(qiáng)大的EnumStream模塊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容