Elixir实战: 4 数据抽象 (2)处理层次数据
myzbx 2025-01-21 20:00 36 浏览
4.2 处理层次数据
在本节中,您将扩展 TodoList 抽象以提供基本的 CRUD 支持。您已经通过 add_entry/2 和 entries/2 函数解决了 C 和 R 部分。现在,您需要添加更新和删除条目的支持。为此,您必须能够唯一标识待办事项列表中的每个条目,因此您将首先为每个条目添加唯一的 ID 值。
4.2.1 生成 ID
在向列表添加新条目时,您将自动生成其 ID 值,使用递增的整数作为 ID。要实现这一点,您需要做几件事:
- 将待办事项列表表示为一个结构体。您需要这样做,因为待办事项列表现在必须保留两条信息:条目集合和下一个条目的 ID 值。
- 使用条目的 ID 作为键。到目前为止,在将条目存储在集合中时,您使用了条目的日期作为键。您将更改此设置,改为使用条目的 ID。这将使快速插入、更新和删除单个条目成为可能。现在每个键将恰好有一个值,因此您不再需要 MultiDict 抽象。
让我们开始实现这个。以下列表中的代码包含模块和结构定义。
列表 4.11 TodoList 结构 (todo_crud.ex)
defmodule TodoList do
defstruct next_id: 1, entries: %{}
def new(), do: %TodoList{}
...
end描述待办事项列表的结构
创建一个新实例
待办事项列表现在将表示为一个包含两个字段的结构体。字段 next_id 包含将在添加到结构体时分配给新条目的 ID 值。字段 entries 是条目的集合。如前所述,您现在使用的是一个映射,键是条目 ID 值。
在结构体定义期间, next_id 和 entries 字段的默认值被立即指定。因此,在创建新实例时,您不必提供这些。 new/0 函数创建并返回结构体的一个实例。
接下来,是时候重新实现 add_entry/2 函数了。它需要做更多的工作:
- 设置要添加的条目的 ID。
- 将新条目添加到集合中。
- 增加 next_id 字段。
这是代码。
清单 4.12 为新条目自动生成 ID 值 (todo_crud.ex)
defmodule TodoList do
...
def add_entry(todo_list, entry) do
entry = Map.put(entry, :id, todo_list.next_id)
new_entries = Map.put(
todo_list.entries,
todo_list.next_id,
entry
)
%TodoList{todo_list |
entries: new_entries,
next_id: todo_list.next_id + 1
}
end
...
end设置新条目的 ID
将新条目添加到条目列表中
更新结构
这里发生了很多事情,所以让我们一步一步来。
在函数体内,您首先使用存储在 next_id 字段中的值更新条目的 id 值。注意您如何使用 Map.put/3 来更新条目映射。输入映射可能不包含 id 字段,因此您无法使用标准的 %{entry | id: next_id} 技术,该技术仅在 id 字段已经存在于映射中时有效。一旦条目被更新,您将其添加到条目集合中,并将结果保存在 new_entries 变量中。
最后,您必须更新 TodoList 结构实例,将其 entries 字段设置为 new_entries 集合,并递增 next_id 字段。基本上,您在结构中进行了复杂的更改,修改了多个字段以及输入条目(因为您设置了其 id 字段)。
对于外部调用者,整个操作将是原子的。要么一切都发生,要么在发生错误的情况下,什么都不会发生。这是不可变性的结果。添加条目的效果仅在 add_entry/2 函数完成并将其结果赋值给变量时对其他人可见。如果出现问题并引发错误,则任何转换的效果将不可见。
值得重申的是,如第 2 章所提到的,新待办事项列表(由 add_entry/2 函数返回的那个)将尽可能与输入的待办事项列表共享内存。
完成了 add_entry/2 函数后,您需要调整 entries/2 函数。这将更加复杂,因为您更改了内部结构。之前,您保持了日期到条目的映射。现在,条目使用 id 作为键进行存储,因此您必须遍历所有条目并返回在给定日期内的条目。
清单 4.13 过滤特定日期的条目 (todo_crud.ex)
defmodule TodoList do
...
def entries(todo_list, date) do
todo_list.entries
|> Map.values()
|> Enum.filter(fn entry -> entry.date == date end)
end
...
end取值
过滤特定日期的条目
此功能首先使用 Map.values/1 从 entries 映射中获取条目。然后,仅使用 Enum.filter/2 获取在给定日期的条目。
您可以检查您的新版本待办事项列表是否正常工作:
$ iex todo_crud.ex
iex(1)> todo_list = TodoList.new() |>
TodoList.add_entry(%{date: ~D[2023-12-19], title: "Dentist"}) |>
TodoList.add_entry(%{date: ~D[2023-12-20], title: "Shopping"}) |>
TodoList.add_entry(%{date: ~D[2023-12-19], title: "Movies"})
iex(2)> TodoList.entries(todo_list, ~D[2023-12-19])
[
%{date: ~D[2023-12-19], id: 1, title: "Dentist"},
%{date: ~D[2023-12-19], id: 3, title: "Movies"}
]这按预期工作,您甚至可以看到每个条目的 ID 值。还要注意, TodoList 模块的接口与之前的版本相同。您进行了多项内部修改,改变了数据表示,几乎重写了整个模块。然而,模块的客户端不需要进行更改,因为您保持了函数的相同接口。
这并不是革命性的——这是将行为封装在适当选择的接口后面的经典好处。然而,它展示了在处理无状态模块和不可变数据时,如何构建和推理更高级别的类型。
4.2.2 更新条目
现在您的条目已经有了 ID 值,您可以添加额外的修饰操作。让我们实现 update_entry 操作,它可以用于修改待办事项列表中的单个条目。
此功能将接受一个条目 ID 和一个更新器 lambda,该 lambda 将被调用以更新条目。这将类似于 Map.update 。该 lambda 将接收原始条目并返回其修改版本。为了保持简单,如果给定 ID 的条目不存在,该功能不会引发错误。
以下代码片段演示了用法。在这里,您修改一个 ID 值为 1 的条目的日期:
iex(1)> TodoList.update_entry(
todo_list,
1,
&Map.put(&1, :date, ~D[2023-12-20])
)要修改的条目的 ID
修改条目日期
实现如下所示。
清单 4.14 更新条目 (todo_crud.ex)
defmodule TodoList do
...
def update_entry(todo_list, entry_id, updater_fun) do
case Map.fetch(todo_list.entries, entry_id) do
:error ->
todo_list
{:ok, old_entry} ->
new_entry = updater_fun.(old_entry)
new_entries = Map.put(todo_list.entries, new_entry.id, new_entry)
%TodoList{todo_list | entries: new_entries}
end
end
...
end不可进入—返回未更改的列表
条目存在—执行更新并返回修改后的列表
让我们来分析一下这里发生了什么。首先,您使用 Map.fetch/2 查找具有给定 ID 的条目。如果条目不存在,函数将返回 :error ,否则返回 {:ok, value} 。
在第一种情况下,如果条目不存在,则返回列表的原始版本。否则,您必须调用更新器 lambda 以获取修改后的条目。然后,将修改后的条目存储到条目集合中。最后,将修改后的条目集合存储在 TodoList 实例中并返回该实例。
4.2.3 不可变的层次更新
您可能没有注意到,但在前面的示例中,您对一个不可变的层次结构进行了深度更新。让我们来分析一下当您调用 TodoList .update_entry(todo_list, id, updater_lambda) 时发生了什么:
- 您将目标条目放入一个单独的变量中。
- 您调用更新程序,它将修改后的条目返回给您。
- 您调用 Map.put 将修改后的条目放入条目集合中。
- 您返回包含新条目集合的待办事项列表的新版本。
请注意,第 2、3 和 4 步是您转换数据的步骤。每个步骤都会创建一个包含转换数据的新变量。在每个后续步骤中,您将使用这些数据并更新其容器,再次通过创建其转换版本来实现。
这就是如何处理不可变数据结构。如果你有层次化的数据,你不能直接修改位于其树深处的部分。相反,你必须沿着树向下走到需要修改的特定部分,然后对其及其所有祖先进行转换。结果是整个模型的一个副本(在这种情况下,是待办事项列表)。如前所述,两个版本——新版本和旧版本——将尽可能共享内存。
提供的帮助者
尽管所提出的技术有效,但对于更深的层级可能会变得繁琐。请记住,要更新层级深处的一个元素,您必须走到该元素并更新其所有父级。为了简化这一过程,Elixir 提供了对更优雅的深层次层级更新的支持。
让我们看一个基本的例子。假设待办事项列表被表示为一个简单的映射,其中键是 ID,值是由字段组成的普通映射。让我们创建一个这样的待办事项列表的实例:
iex(1)> todo_list = %{
1 => %{date: ~D[2023-12-19], title: "Dentist"},
2 => %{date: ~D[2023-12-20], title: "Shopping"},
3 => %{date: ~D[2023-12-19], title: "Movies"}
}现在,假设你改变主意,想去剧院而不是看电影。原始结构可以优雅地使用 Kernel.put_in/2 宏进行修改:
iex(2)> put_in(todo_list[3].title, "Theater")
%{
1 => %{date: ~D[2023-12-19], title: "Dentist"},
2 => %{date: ~D[2023-12-20], title: "Shopping"},
3 => %{date: ~D[2023-12-19], title: "Theater"}
}层级更新
条目标题已更新。
这里发生了什么?在内部, put_in/2 做的事情类似于你所做的。它递归地遍历到所需的元素,进行转换,然后更新所有父级。请注意,这仍然是一个不可变操作,这意味着原始结构保持不变,你必须将结果赋值给一个变量。
要能够进行递归遍历, put_in/2 需要接收源数据和目标元素的路径。在前面的示例中,源数据提供为 todo_list ,路径指定为 [3].title 。宏 put_in/2 然后沿着该路径向下遍历,在上升过程中重建新的层次结构。
值得注意的是,Elixir 提供了类似的替代方案用于数据检索和更新,形式为 get_in/2 、 update_in/2 和 get_and_update_in/2 宏。这些是宏的事实意味着您提供的路径在编译时被评估,无法动态构建。
如果您需要在运行时构建路径,有相应的函数可以将数据和路径作为单独的参数接受。例如,Elixir 还包括 put_in/3 宏,可以如下使用:
iex(3)> path = [3, :title]
iex(4)> put_in(todo_list, path, "Theater") 使用在运行时构建的路径
函数和宏,例如 put_in ,依赖于 Access 模块,该模块允许您处理键值结构,例如映射。您还可以创建自己的抽象以与 Access 一起使用。您需要实现 Access 合约所需的几个函数,然后 put_in 及相关宏和函数将知道如何与您自己的抽象一起工作。有关更多详细信息,请参阅官方 Access 文档 (https://hexdocs.pm/elixir/Access.xhtml)。
练习:删除条目
您的 TodoList 模块几乎完成。您已经实现了创建 ( add_entry/2 )、检索 ( entries/2 ) 和更新 ( update_entry/3 ) 操作。最后要实现的是 delete_entry/2 操作。这很简单,留给您作为练习。如果您遇到困难,解决方案在源文件 todo_crud.ex 中提供。
4.2.4 迭代更新
到目前为止,您一直在手动逐个进行更新。现在,是时候实施迭代更新了。想象一下,您有一个原始列表描述条目:
$ iex todo_builder.ex
iex(1)> entries = [
%{date: ~D[2023-12-19], title: "Dentist"},
%{date: ~D[2023-12-20], title: "Shopping"},
%{date: ~D[2023-12-19], title: "Movies"}
]现在,您想创建一个包含所有这些条目的待办事项列表实例:
iex(2)> todo_list = TodoList.new(entries)显然, new/1 函数执行了待办事项列表的迭代构建。你如何实现这样的函数?事实证明,这很简单。
清单 4.15 迭代构建待办事项列表 (todo_builder.ex)
defmodule TodoList do
...
def new(entries \\ []) do
Enum.reduce(
entries,
%TodoList{},
fn entry, todo_list_acc ->
add_entry(todo_list_acc, entry)
end
)
end
...
end初始累加器值
迭代更新累加器
要迭代地构建待办事项列表,您依赖于 Enum.reduce/3 。回想一下第 3 章, reduce 用于将可枚举的内容转换为其他任何内容。在这种情况下,您将一组原始的 Entry 实例转换为 TodoList 结构的一个实例。因此,您调用 Enum.reduce/3 ,将输入列表作为第一个参数,将新的结构实例作为第二个参数(初始累加器值),以及在每一步中调用的 lambda。
每个输入列表中的条目都会调用 lambda。它的任务是将条目添加到当前累加器( TodoList 结构)中,并返回新的累加器值。为此,lambda 委托给已经存在的 add_entry/2 函数,反转参数顺序。参数需要反转,因为 Enum.reduce/3 调用 lambda,传递迭代元素(条目)和累加器( TodoList 结构)。相比之下, add_entry 接受一个结构和一个条目。
注意,您可以借助捕获运算符使 lambda 定义更加简洁:
def new(entries \\ []) do
Enum.reduce(
entries,
%TodoList{},
&add_entry(&2, &1)
)
end反转参数的顺序并委托给 add_entry/2
无论您使用这个版本还是之前的版本完全取决于您的个人喜好。
4.2.5 练习:从文件导入
现在,是时候让你练习一下了。在这个练习中,你将从一个以逗号分隔的文件中创建一个 TodoList 实例。
假设您在当前文件夹中有一个 todos.csv 文件。文件中的每一行描述一个单独的待办事项条目:
2023-12-19,Dentist
2023-12-20,Shopping
2023-12-19,Movies您的任务是创建一个额外的模块 TodoList.CsvImporter ,该模块可以用于从文件内容创建一个 TodoList 实例:
iex(1)> todo_list = TodoList.CsvImporter.import("todos.csv")为了简化任务,假设文件始终可用且格式正确。还假设逗号字符不会出现在条目标题中。
这通常不难做到,但可能需要一些破解和实验。以下是一些提示,可以引导您朝正确的方向前进。
首先,创建一个具有以下布局的单个文件:
defmodule TodoList do
...
end
defmodule TodoList.CsvImporter do
...
end始终以小步骤进行工作。实现部分计算,然后使用 IO.inspect/1 将结果打印到屏幕上。我无法过分强调这一点的重要性。此任务需要一些数据管道处理。以小步骤工作将使您能够逐步前进,并验证您是否在正确的轨道上。
您应该采取的一般步骤如下:
- 打开一个文件并逐行查看,删除每行中的 \n 。提示:使用 File.stream!/1 、 Stream.map/2 和 String.trim_trailing/2 。你在第三章中做过这个,当时我们谈论流,在过滤长度超过 80 个字符的行的示例中。
- 使用 Stream.map ,将从上一步获得的每一行转换为待办事项条目。
- 将该行转换为 [date_string, title] 列表,使用 String.split/2 。
- 将日期字符串转换为日期,使用 Date.from_iso8601! ( https://hexdocs.pm/elixir/Date.xhtml#from_iso8601!/2)。
- 创建待办事项列表条目(形状为 %{date: date, title: title} 的地图)。
步骤 2 的输出是一个可枚举的待办事项条目。将该可枚举对象传递给您最近实现的 TodoList.new/1 函数。
在每个步骤中,您将接收一个可枚举对象作为输入,转换每个元素,并将生成的可枚举对象传递给下一个步骤。在最后一步,生成的可枚举对象被传递给已经实现的 TodoList.new/1 ,并创建待办事项列表。
如果你以小步骤工作,就更难迷失方向。例如,你可以先打开一个文件并将每一行打印到屏幕上。然后,尝试去除每一行末尾的换行符并将它们打印到屏幕上,依此类推。
在每一步转换数据时,您可以使用 Enum 函数或 Stream 模块中的函数。开始时使用 Enum 模块中的急切函数可能会更简单,并使整个过程正常工作。然后,尽量用 Stream 对应的函数替换尽可能多的 Enum 函数。请回忆第 3 章, Stream 函数是惰性和可组合的,这可以减少操作所需的中间内存量。如果您迷路了,解决方案在 todo_import.ex 文件中提供。
与此同时,我们几乎完成了对更高级抽象的探索。我们将简要讨论的最后一个主题是 Elixir 的多态实现方式。
相关推荐
- 如何设计一个优秀的电子商务产品详情页
-
加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...
- 怎么在JS中使用Ajax进行异步请求?
-
大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...
- 中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革
-
前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...
- 前端监控 SDK 开发分享_前端监控系统 开源
-
一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...
- Ajax 会被 fetch 取代吗?Axios 怎么办?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...
- 前端面试题《AJAX》_前端面试ajax考点汇总
-
1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...
- Ajax 详细介绍_ajax
-
1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...
- 6款可替代dreamweaver的工具_替代powerdesigner的工具
-
dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...
- 我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊
-
接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...
- 福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光
-
时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...
- 2021年超详细的java学习路线总结—纯干货分享
-
本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...
- 不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!
-
Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...
- 2025 年 Python 爬虫四大前沿技术:从异步到 AI
-
作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...
- 最贱超级英雄《死侍》来了!_死侍超燃
-
死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...
- 停止javascript的ajax请求,取消axios请求,取消reactfetch请求
-
一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)
