使用光学和标准单子对“JQ”和导线系统进行泛化

2020-10-08 09:09:49

大家好!今天我将谈论像JQ和XPath这样的遍历系统,我们将发现哪些属性使它们变得有用,然后看看我们如何使用(几乎全部)Pre-ol来复制它们在Haskell中最有用的行为!现有的标准Haskell工具!我们走吧!

首先,我得承认,“穿越系统”是我刚刚想出来的一个名字,如果你搜索一下,很可能什么也找不到(除非这篇文章真的是在😉上找到的)。

遍历系统允许您深入研究一段数据,并可能允许您边走边获取、查询和编辑结构,同时维护对结构的其他部分的引用以影响您的工作。大多数遍历系统的目标是使其尽可能无痛且简明。事实证明,这种事情对于操作JSON、查询HTML和CSS、处理CSV,甚至只是处理标准的Haskell记录和数据类型都非常有用。

您可能听说过的现有遍历系统的一些很好的例子包括用于操作和查询JSON的出色的JQ实用程序、用于查询XML的XPath语言,以及Clojure中的曲折数据处理系统。尽管这些系统乍看起来可能截然不同,但它们都以简明的方式实现了许多相同的目标:操作和查询数据。

这些系统之间的相似之处让我很感兴趣!它们看起来如此相似,但在结构、语法和现有技术方面似乎仍然没有什么共同之处。他们为每种新的数据类型重新发明了轮子!理想情况下,我们可以识别每个系统中的有用行为,并构建适用于任何数据类型的通用系统。

这篇文章就是试图做到这一点;我们将看一看这些系统做得很好的一些事情,然后我们将使用标准工具在Haskell中重新构建它们,同时对数据类型进行抽象!

对于任何认识我的人来说,我的第一个想法是看光学(即镜头和遍历),这应该不足为奇。一般说来,我发现光学眼镜解决了我的很多问题,但在这种情况下,它们特别合适!光学本质上处理的是深入研究数据并以结构化和组合方式查询或更新数据的想法。

此外,光学还允许对其工作的数据类型进行抽象。有一些预先存在的光学库,可以通过Lens-Aeson与JSON一起工作,也可以通过Taggy-Lens与HTML一起工作。我已经编写了用于处理CSV甚至正则表达式的光学库,所以我可以自信地说,它们是一个非常适合数据操作的工具。

碰巧光学原理良好,在数学上也是可靠的,所以它们是研究像这样的系统可能具有的特性的一个很好的工具。

然而,光学本身并不能提供我们需要的一切!光学是相当迟钝的,事实上我写了一整本书来帮助教授它们,当涉及到构建更大的表达式时,它们缺乏清晰度和易用性。在一个数据结构的一个部分上工作,同时引用同一结构的另一个部分中的数据也是相当困难的。我希望在这篇文章中解决这些缺点中的一些。

在这篇特别的帖子中,我最感兴趣的是解释哈斯克尔遍历系统的框架,我们将使用许多标准的MTL Monad Transformers,以及镜头库中的许多组合器。你不需要深入了解其中的任何一个,就能了解到底发生了什么,但我不会在这里深入解释它们,所以如果你缺乏一点背景知识,你可能需要寻找其他地方。

在我们进行的过程中,我将演示几个示例,所以让我们设置一些数据。我将在JQ和Haskell中工作,以便对它们进行比较,因此我们将在JSON和Haskell中设置相同的数据。

{";员工";:[{";id";:";1";,";姓名";:";鲍勃";,";宠物";:[{";姓名";:";洛奇";,";类型";:";猫";},{";名称";:";Bullwinkle";,";类型";:";狗";}]},{";id";:";2";,";名称";:";Sally";,";宠物";:[{";名称";:";Inigo";,";类型";:";猫";}]}],";工资";:{";1";:12,";2";:15}}。

这里使用的是Haskell表示的相同数据,并为每个记录域生成了光学数据。

Data Company=Company{_Staff::[Employee],_Payments::M.Map Int Int}派生显示数据Pet=Pet{_petName::string,_petType::string}派生显示数据Employee=Employee{_EmploeId::int,_EmploeName::string,_EmploePets::[Pet]}派生显示Make Lens';';';Company Make Lens';';Pet Make Lens';';员工公司::COMPANY COMPANY=Company[员工1";鲍勃";[Pet";Rocky&34;";CAT";Pet";Bullwinkle";";Dog";],员工2";Sally&34;[Pet";Inigo";";cat";],员工2";Sally";[Pet";Inigo";";CAT";]](M.fromList[(1,12),(2,15)])。

让我们深入到几个示例查询中来试试看吧!首先是一个简单的问题,让我们编写一个查询来查找我们任何员工拥有的所有宠物。

$cat company.json|JQ';.Staff[].perts[]|SELECT(.type==";cat";)';{";name";:";Rocky";,";type";:";cat";}{";name";:";inigo";,";type";:";cat";}。

我们查看员工密钥,然后枚举该列表,然后为每个员工枚举他们的猫!最后,我们过滤掉任何不是猫的东西。

我们可以在这里辨认出一些穿越系统的特征。JQ通过提供一条通往我们想去的地方的路径,让我们能够更深入地研究我们的结构。它还允许我们使用[]运算符枚举许多可能性,该运算符将每个值一个接一个地转发到管道的其余部分。最后,它允许我们使用SELECT过滤结果。

>;>;>;to ListOf(员工。折好了。雇员宠物。折好了。FilteredBy(petType.。仅";猫";))公司[宠物{_petName=";Rocky";,_petType=";猫";},宠物{_petName=";Inigo";,_petType=";猫";}]。

在这里,我们使用了一种眼镜,它可以折叠到每位员工身上,然后折叠到他们的每一只宠物身上,同样也只过滤了猫咪。在这里,我们使用的是一种光学元件,它可以折叠到每位工作人员身上,然后再折叠到他们的每一只宠物身上,同样只过滤猫。

它们各自都允许枚举多个值,在JQ中使用[],在光学中使用Folded。

太棒了!到目前为止,我们没有遇到任何问题!我们已经开始看到这两者之间有很多相似之处,我们使用光纤的解决方案很容易推广到任何数据类型。

$cat join.json|JQ';.Staff[]|.name为$PersonName|.perts[]|";\(.name)属于\($PersonName)";';";Rocky属于Bob";";Bullwinkle属于Bob";";Inigo属于Sally&34;

在这里,我们看到了JQ中的一个新特性,即在我们继续深入研究结构的同时,能够维护对结构的一部分的引用,以备以后使用。我们在枚举每个员工时重新抓取他们的名字,并将其保存到$PersonName中,以便稍后参考。然后,我们枚举每一只宠物,并使用字符串插值来描述每只宠物的主人。

如果我们试图依靠光学本身,嗯,这是可能的,但不幸的是,这就是一切开始崩溃的地方,看看这个绝对的烂摊子:

所有者::[字符串]所有者=公司^..。(工作人员。折好了。Reindexed_ployeeName selfIndex<;。雇员宠物。折好了。PetName)。使用索引。致(\(eName,pname)->;pname<;>;";属于";<;>;eName)>;>;所有者[";Rocky属于bob&34;,";Bullwinkle属于bob&34;,";Inigo属于Sally&34;]。

你可以打赌,没有人会把这叫做“容易读懂”(Easy";Easy&34;)。见鬼,我写了一本关于光学的书,但我还是花了几次努力才弄清楚托架需要放在哪里!

光学器件很适合处理单个值流,但在更复杂的表达式上就差得多了,特别是那些需要引用链中较早出现的值的表达式。让我们看看当我们在Haskell构建我们的遍历系统时,如何解决这些缺点。

仅供观众席中的JQ爱好者使用,我将向您展示这个备用版本,它使用了JQ为您所做的一点魔力。

$cat company.json|JQ';.Staff[]|";\(.perts[].name)属于\(.name)";';";Rocky属于Bob";";Bullwinkle属于Bob";";Inigo属于Sally&34;

根据您的经验,😬可能不那么神奇,但更令人困惑。由于最终表达式包含枚举(即\(.perts[].name)),因此JQ将为枚举中的每个值展开一次最终项。这真的很酷,但不幸的是,在我看来,这有点不那么原则性,很难理解。

无论如何,行为是一样的,我们还没有令人满意地在哈斯克尔复制它,让我们看看我们能做些什么!

在哈斯克尔,我们喜欢我们的嵌入式😂;如果你给哈斯克勒一个要解决的问题,你可以打赌,他们十有八九会用定制的Monad和DSL DSL来解决这个问题。嗯,我很抱歉地告诉你,我也没什么不同!

我们将使用单元体来解决最后一个光学解决方案的可读性问题,但问题是…。哪个单子?

由于我们目前正在做的所有事情都是查询数据,我们可以利用备受尊敬的阅读器Monad来为我们的查询提供上下文。

下面是当我们将Reader Monad与相对鲜为人知放大组合符一起使用时,最后一个查询是什么样子:

所有者';:阅读器公司[字符串]所有者=Do放大(员工。折叠)$do PersonName<;-查看EmploeName放大(EmploePets.。折叠)$do AnimalName<;-view petName return[AnimalName<;>;";属于";<;>;>;>;runReader所有者&39;公司[";Rocky属于Bob&34;,";Bullwinkle属于Bob&34;,";Inigo属于Sally&34;]。

我不会在这里解释Reader Monad本身是如何工作的,所以如果你在这方面有点不确定,你可能会想先熟悉一下。

至于放大,它是镜头库中的一个组合器,它以光学和动作为参数。它使用光学元件聚焦阅读器环境的一个子集,然后以该数据子集为焦点在阅读器内运行操作。就这么简单!

还有一件事!放大可以接受聚焦多个元素的折叠,在这种情况下,它将为每个焦点运行一次操作,然后使用半组将所有结果组合在一起。在本例中,在返回结果之前,我们将结果包装在一个列表中,因此AMPLIZE将继续并自动为我们将所有结果连接在一起。我们不需要自己编写任何代码就可以从Amplify获得如此多的功能,这真是太棒了!

我们可以看到,用这种风格重写问题使它更容易阅读。它使我们可以在使用光学器件下降并在任何给定的地点四处探查时暂停一下。(这句话的意思是:“当我们使用光学设备下降并在任何给定的地点四处闲逛时,我们可以暂停一下。”由于它是一个单体,而我们正在使用do-notation,我们可以很容易地将任何中间结果绑定到名称中,以便稍后引用;名称将正确引用当前迭代中的值!通过查看每个嵌套的do-notation块的缩进,我们可以清楚地指示所有绑定的范围,这也很好。

根据您的个人风格,您可以直接使用(->;)monad来编写此表达式,甚至可以完全省略缩进;尽管我个人并不推荐这样做。如果你很好奇,下面是我不推荐你写这篇文章的方式:。

所有者';';::company->;[string]所有者';';=do放大(员工。折叠)$do eName<;-查看EmploeName放大(EmploePets.。已折叠)$do pname<;-view petName Return[pname<;>;";属于";<;>;eName]。

好吧!。继续下一步!比方说,根据我们公司的政策,任何养狗的人都要加薪5美元!嘿,这里的规矩不是我定的,🤷‍♂️。请注意,这一次我们运行的是更新,而不仅仅是查询!

Cat company.json|JQ';[.Staff[]|SELECT(.perts[].type==";dog";)|.id]作为$People WithDog|.payments[$PeopleWithDog[]]+=5';{";Staff";:[{";id";:";1";,";name";:";bob";,";perts";;";bob";,";pets";:";bob";,";perts";:";bob";,";:[{";Name";:";Rocky";,";type";:";cat";},{";name";:";Bullwinkle";,";type";:";dog";}]},{";id";:";2";,";名称";:";莎莉";,";宠物";::";名称";,";类型";:";猫";}],";工资";:{";1";:17,";2";:15}}。

我们首先扫描员工,看看谁值得升职,然后反复检查他们的每个身份证,提高他们的工资,果然奏效了!。

我承认,我花了几次尝试才在JQ;中做到了这一点。如果您不小心,您将以这样一种方式枚举:JQ无法跟踪您的引用,并且您将无法编辑原始对象的正确部分。?例如,这是我第一次尝试做这类事情:

$cat company.json|JQ';AS$COMPANY|.Staff[]|SELECT(.pets[].type==";DOG";).id|$company.payments[.]+=5';jq:error(at<;stdin>;:28):尝试访问{";Staff";的元素";Sales";:[{";id";:";1";,";name";]时路径表达式无效,共{";员工";:[{";id";:";1";,";name";;..。

在这种情况下,JQ看起来不能编辑我们存储为变量的内容;这有点令人惊讶,但我想这已经够公平的了。

这类任务很棘手,因为它涉及一个区域的枚举,存储这些结果,然后在另一个区域枚举和更新!这在JQ绝对是可能的,但JQ的一些魔力让人很难一目了然地知道什么能奏效,什么不能。

SalaryBump::State Company()salaryBump=do ID<;-get$toListOf(Staff.。遍历了。FilteredBy(EmploePets.。遍历了。PetType。仅限";狗";)。EmploeId)for_ids$\id';->;工资。Ix id';+=5>;>;>;execState salaryBump company Company{_Staff=[EmployeeId=1,_EmploeName=";bob";,_EmploePets=[Pet{_petName=";Rocky";,_petType=";CAT";},Pet{_petName=";Bullwinkle";,_petType=";},Pet{_petName=";Bullwinkle";,_petType=";},Pet{_petName=";Bullwinkle";,_petType=";Dog";}]},Employee{_EmploeId=2,_EmploeName=";,Sally";,_EmploePets=[Pet{_petName=";Inigo";,_petType=";CAT";}]}],_Employees=FromList[(1,17),(2,15)]}。

您会注意到,现在我们需要更新一个值,而不仅仅是查询,我已经从Reader monad切换到State monad,这允许我们以一种模仿可变状态的方式跟踪我们的公司。

首先,我们依靠光学设备来收集所有养狗的人的身份证。然后,一旦我们获得了这些ID,我们就可以遍历我们的ID并使用每个ID执行更新操作。镜片库包括许多巧妙的组合器,用于在State Monad内部处理光学元件;在这里,我们使用的是以给定的ID有状态地更新工资。For_from Data.Foldable正确地对我们的每个操作进行排序,并逐个应用更新。

当我们在State而不是Reader中工作时,我们需要使用缩放而不是放大;这里是上一个示例的重写,该示例以一种琐碎的方式使用了缩放;但是缩放允许我们在放大之后还可以编辑值!(==#*$$=。

SalaryBump::State Company()salaryBump=do ID<;-zoom(Staff.。遍历了。FilteredBy(EmploePets.。遍历了。PetType。仅";DOG";))$DO对_ID$\id';->;工资使用ployeeId(:[])。IX id';+=5

因此,希望到目前为止,我已经说服您,我们可以用一种数据不可知的方式忠实地重新创建像Haskell中的JQ这样的语言的核心行为!通过交换您的光学设备,您可以在JSON、CSV、HTML或您能想象到的任何东西上使用同样的技术。它利用标准的Haskell工具,因此它与Haskell库很好地结合在一起,并且您保持了Haskell语言的全部功能,因此您可以轻松地编写自己的组合符来扩展词汇量。

剩下的问题是,我们能从这里走向何方?当然,答案是我们可以添加更多的Monad!

虽然我们已经从镜头中过滤了一遍又一遍,用光学来过滤枚举和遍历;但如果我们在DO-NOTION块中也能有同样的能力,那就太好了!Haskell已经为此提供了一个名为Guard的通用标准组合器。无论您使用的是哪种单子,它都会失败。要工作,它取决于您的类型是否具有替代类型的实例;不幸的是,对于我们州来说没有;因此,我们需要寻找另一种方法来获得替代实例😂。

MaybeT Monad变压器是专门为其他Monad类型添加故障而存在的,所以让我们把它集成起来吧!这里的棘手之处在于,我们只想使计算的一个分支失败,而不是整个计算失败!因此,我们需要在任何失败的分支合并回主计算之前捕获它们。

Infix r0%>;(%>;)::Traversal';s e->;MaybeT(State E)a->;MaybeT(State S)[a]l%>;m=do zoom l$do--捕获并嵌入当前分支,这样我们就不会使整个程序失败(<;-Lift$runMaybeT m返回(可能是[](:[])a))。

这为我们的遍历DSL定义了一个方便的新组合器,它允许我们像以前一样缩放,但是MaybeT的添加允许我们很容易地使用Guard修剪分支!

我们确保运行并重新提升我们操作的结果,而不是直接嵌入它们,否则单个失败的保护将导致整个剩余的计算失败,这肯定不是我们想要的!因为每个单独的分支都可能失败,而且我们通常都是以列表的形式收集结果,所以我只是继续将我们的结果作为组合器的一部分嵌入到列表中,这应该会让一切都更容易使用!

让我们试试看吧!我将重写前面的示例,但这一次我们将使用Guard而不是filteredBy。

SalaryBump';';:MaybeT(国有公司)()salaryBump';';=do ID<;-Staff。遍历%>;do isDog<;-EmployeePets。遍历%>;do使用petType(==#34;Dog&34;)警卫(或isDog)使用EmploeId for_IDS$\id&39;->;薪水。IX id';+=5>;>;>;翻转execState公司。RunMaybeT$salaryBump';';Company{_Staff=[Employee{_EmploeId=1,_EmploeName=";,_EmploePets=[Pet{_petName=";Rocky";,_petType=";CAT";},Pet{_petName=";Bullwinkle&34;,_petType=";Dog";},Pet{_petName=";,_petType=";Dog";},宠物{_petName=";Bullwinkle";,_petType=";Dog";}]},员工{_EmploeId=2,_EmploeName=&#。

.