Elixir 简明笔记(十八) --- 数据结构实战

介绍了Elixir的基本数据类型和控制结构,可以写一个小应用来实战一下。针对elixir的数据抽象进行写一个简单的todo应用。

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的内容作为value。Todo模块提供一个增加todo的函数。

初步实现

从上面的使用方法来看,TodoList模块有一个new函数,用来创建一个todo“实例”。而todo实际数据结构非常合适哈希结构,这里选择了HashDict。

defmodule TodoList do

    def new, do: HashDict.new

end

剩下就是实现增加todo的函数。可以使用HashDict.update/4函数,可以实现改功能

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函数提供四个参数,第一个是要操作的hashdict,第二个是key,第三个是value,如果所传的key对于的value不存在,就调用第四个lambda函数。匿名函数接收一个存在的value作为参数,返回一个列表。使用iex todo_list.ex运行:

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"]}]>

因为elixir的数据是不可变的,因此一直在针对todo_list进行重新绑定。

下面要实现的一个方法则是通过key(date元组)来获取相应的title内容

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函数可以通过key读取value,当然也可以在value不存在的时候返回一个默认的值。

抽象封装

上述的实现完全可以work。可是还可以针对HashDict做出更高级的抽象。然后让客户端的代码看起来可读性更高。实现一个针对key和value的函数的模块:

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结构

目前为止,经过简单的抽象,已经让Todo的客户端代码变得简洁。可是在调用的时候,key传一个tuple还是让阅读性降低,既然todo是哈稀结构,那么参数也可以传一个哈稀结构就非常匹配。因此可以使用map来当成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

前面我们实现了C和R两个操作,接下来将会实现todo应用的修改和删除条目操作。通常而言,一个item条目,拥有一个id,这样对这个条目的操作可以借助id来做 关系的处理。下面对todo进行修改,客户端的代码还是一致,通过entry的title和value来创建todo,每一个条目的id都是自增的。这里使用了elixir的一种新的数据协议,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

上面的代码,定义了一个struct,包含两个字段,一个是自增的当前id,默认为1。另外这是todo的条目,默认是一个空的HashDict。TodoList.new/0 方面很简单,初始化一个todo模块的实例。

TodoList.add_entry/2 是增加一个条目,第一个参数使用了模式匹配,将传入的todo实例进行模式匹配,第二个参数是用来增加的条目。新增的条目是一个map,因此使用put函数增加一个key为id,id的值为当前自增的id,entries是一个HashDict。它的key都是自增id,值都在具体的条目,因此使用put函数新建一个new_entries。然后需要自增id,最后再使用struct的更新语法更新struct。因为所更新的new_id以及新entriesHashDict。对于已经存在的字段,可以使用|语法更新。

最后的 TodoList.entires/2 函数的第一个参数也有模式匹配,因为函数内不需要使用todo_list,因此可以省略而不用写成%TodoList{entries: entries}=todo_list。具体逻辑则通过Stream模块进行迭代过滤,找出date与参数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更能,下一个功能则是下面

todo条目的更新和删除

下面实现更新和删除的功能。可以使用HashDict.update来更新一个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宏,如果是常规的编程语言,大概思路可能如下:

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

实现delete方法很简单,调用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

总结

Elixir提供的数据类型比较丰富,并且发展也很快,随着Erlang的进化,elixir也在不断的跟进。之前刚查询HashDict的一些函数,请教了一个朋友,他说,为啥不用map。原来最新的1.2.4版本map不象之前1.0版本那样性能不足以支持大数据。最新的map已经对多item的性能进行了优化,map可以取代HashDict。

无论HashDict还是Map。这些基本结构的操作都少不了常规的方法,具体选取可以跟进实际应用场景结合最新的文档。所谓的常规方法免不了需要进行迭代。我们知道递归可以循环,elixir还提供了一些高级函数封装隐藏了这些迭代细节。下面将会介绍强大的EnumStream模块

你可能感兴趣的:(Elixir 简明笔记(十八) --- 数据结构实战)