介绍了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以及新entries
的HashDict
。对于已经存在的字段,可以使用|
语法更新。
最后的 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还提供了一些高级函数封装隐藏了这些迭代细节。下面将会介绍强大的Enum
和Stream
模块