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模块

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容