$head facts/Recipes马提尼<;-伦敦干吉尼马提尼<;-干苦艾酒轻拉姆代基里<;-酸橙汁白兰地<;-简单糖浆玛格丽塔<;-布兰科龙舌兰或雷帕萨多龙舌兰<;-酸橙汁玛格利塔<;-橙汁玛格丽塔<;-石灰楔。。。
天哪,不多。我可以在吧台上添加什么来扩展我的选择?
$cat结果/购物清单伦敦干杜松子酒->;吉姆莱特伦敦杜松子酒->;martinichampagne->;airmailcognac->;在床单之间Hery->;雪莉柯布勒胡姆阿格里科尔->;钛冲压机
不,你错了,这很聪明。仔细看一看Daiquiri:菜谱上要求加酸橙汁,但我们的酒吧里没有酸橙汁。我们只有酸橙。那么为什么它会显示为可混合的呢?
我很高兴你这么问。这些结果是由我编写的一个名为Mixologician的小型数据记录程序生成的。
调酒师知道一些规则。它知道酸橙可以制成酸橙汁——酸橙楔、酸橙皮等等。它甚至知道酸橙皮加糖可以制成酸橙热饮——所以它知道,我们离制作小金环只有一种原料,尽管从技术上讲,我们没有一种饮料需要的单一原料。
我喜欢鸡尾酒。从前,我喜欢去餐馆点鸡尾酒。但由于某种原因,我有一段时间没去餐馆了,而是在家里做鸡尾酒。
但我不想每次都混合同样的鸡尾酒。我喜欢这家餐厅的鸡尾酒点餐体验的很大一部分是多样性:我经常光顾的每一家时髦餐厅都有自己的鸡尾酒菜单——它有自己独特的一套饮料,里面有我从未听说过的配料。
看,我没有一个非常全面的酒类收藏。我有基本的,当然,在隔离的过程中,我获得了一些更高级的成分。但花式配料通常不是万能的:我曾经买过一瓶Amaro Nono来混合纸飞机。但事实证明我不喜欢真正的纸飞机。所以现在我只喝了97.1%的阿玛罗诺诺,与此无关。1.
所以我写了一个小程序来告诉我:考虑到我现在在酒吧里拥有的东西,我应该添加什么才能让我最大限度地制作新鸡尾酒。或者换句话说,我最有效的购买方式是什么——我最“受阻”的成分是什么
我想这将是一个尝试Datalog的好机会,我以前听过Datalog这个词。
现在,你可能会认为这听起来像是一个小问题,你可以编写几行Python,根据你拥有的和你需要的进行集减法运算,然后过滤掉只缺少一种成分的鸡尾酒。
逻辑编程对我来说似乎合适的原因是这些“生产规则”的想法:可以成为(或结合成为)其他成分的成分。
现在,我并不是说用你最喜欢的脚本语言来建模会很困难,但如果不使用猜测和检查之类的暴力搜索,如何实现这一点已经不太明显了。并不是说这有什么问题;它感觉更适合逻辑编程。
或者不是!谁知道呢。我对逻辑编程一无所知。这整件事只是一个尝试的借口。
与所有代码一样,该代码也在GitHub上。你可以浏览一下这个项目——我建议从测试开始,因为它们提供了一种关于它如何工作的知识指南。
但我敢说,代码本身并没有那么有趣:如果你已经知道数据日志,那么它就微不足道了;如果你不这样做,这是难以理解的。
因此,我将一件一件地浏览最终结果,并讨论我在这一过程中犯下的一些错误。由于这是我第一次接触Datalog,我花了一段时间才能够“思考”Datalog希望我的方式——我认为我在这一过程中犯的错误比最终结果有趣得多。
自定义类型使代码的其余部分更具可读性,并让编译器捕捉到愚蠢的错误,比如混淆关系的顺序。
symbol就是Datalog所说的“字符串”——我指出这一点是因为如果你习惯于LISP、Ruby或其他一些使用不同术语的语言,可能会有点困惑。
我在思考关系时遇到了很多困难,直到我有了一个事后看来非常明显的见解:我实际上一直在关系数据库中使用关系。因此,把Has想象成SQL中的一个表,只包含一列,可能会很有用。这不完全是一回事,但我发现这是思考如何“塑造”我的数据的一个不错的起点。
那个输入行只是从文本文件中加载一个成分列表——每行一种成分。如果愿意,可以在我们的表中插入行——除了在数据日志中,“行”称为“事实”
这种关系更有趣:这些是我们的食谱。虽然在传统语言中,我们可能会认为食谱是{name:string,components:List<;string>;},我们必须在某种程度上改变事物,以适应关系的“表格式”表示。
同样,我们只是从纯文本文件加载数据。默认情况下,Souffle希望读取以制表符分隔的值,但我制作了一个自定义分隔符,因为我认为它的读取效果更好,所以我不必处理制表符。没人想处理标签。
.decl产生(输入:成分,输出:成分)。输入Begets(文件名=";Begets";,分隔符=";->;";)。输入开始(文件名=";自动开始";,分隔符=";->;";)
Begets是一段我们以后会经常谈论的关系。现在,只要把它想象成一堆像“酸橙生酸橙汁”或“干邑生白兰地”这样的说法——这些成分可以成为其他成分,或者可以作为其他成分。“生儿育女”这个名字很奇怪,有点笨拙;一旦我们看到它的使用方式,我会解释为什么我会选择它而不是make或actsa之类的东西。
我从两个独立的文件加载Begets,因为一个是手工制作的,另一个是自动生成的——我将在稍后讨论食谱时解释为什么会这样。
这些是我们的“多种成分”基本上,想想调味糖浆——看看文件中的一些例子:
我只费心支持两种成分组合,因为这基本上涵盖了所有东西。但是,如果你愿意,你可以添加另一个Composite3()关系或其他东西来支持更多成分。
等等,如果复合材料不能,为什么食谱中可以任意添加许多成分?为什么不在这里做同样的事情,只是做一个“表”的成分组合?
好的,问得好。基本上,我认为这会给这个例子增加一个分散注意力的复杂程度,所以我把它留给读者作为练习。实际上我还没有尝试过,但我认为它会起作用,你甚至可以把“食谱”的概念概括为任何“一种或多种事物的组合”,只有一种类型,所有东西都会很漂亮、优雅,但在博客文章中很难理解。
我不会从文件中加载这个。相反,我基本上是说,“如果x出现在需求关系的第一列,那么它就是一个配方。”您可以将其视为类似于SQL视图,有点像:
我把IsRecipe作为一个助手,因为我认为它比我所说的“所有食谱”更明确
.decl-isingredent(x:成分)isingredent(x):-Needs(ux)。IsingCredit(x):-Begets(x,u)。IsIngredient(x):-产生(ux)。IsingElement(x):-Composite(x,,)。IsingElement(x):-Composite(x,x,x)。IsingElement(x):-Composite(,,x)。
这是另一个助手,但它有多个规则。所有这些都是某种“联合”在一起的。基本上:“x是一种成分,如果它被用在配方中,或者出现在贝格茨规则或复合描述中。”
我们可以在某个地方列出一个文件中的所有成分,但我认为在意向上声明这种关系要好得多。我还想炫耀一下,这是我有生以来第一次正确使用这个词。2.
下一个关系非常简单:Unbuyable是一个类似蛋清的列表,它作为配料出现,但我不想出现在最终输出中,因为我实际上无法在商店购买蛋清。像Begets一样,我也有一些自动生成和手工策划的作品。
第一条规则基本上是说“所有成分都会产生它们自己。”我希望能够写出Begets(x,x)。,但Datalog不允许这种“无限”规则——它需要我为x提供一个域,这就是我们的iSingCredit助手的作用。
这就是为什么这种关系被称为“生儿育女”,而不是“制造”或“生产”之类的东西。最初它被称为Makes,我有一些尴尬的表达方式,比如“如果你有x或者你有y使得(y,x)那么&;mldr”,通过让一切都开始,我能够极大地简化“购物清单”的计算。
第二条规则只是说Begets是传递性的:如果酸橙皮产生酸橙皮,酸橙皮产生酸橙皮,那么酸橙皮产生酸橙皮。实际上,我并没有费心在这样的粒度级别编写规则,但如果我愿意,我可以。
基本上,如果你有一个输入(例如柠檬),而输入产生了其他东西(例如柠檬汁),那么你也有“输出”但是不能使用“输出”这个词,因为它是保留的,如果您尝试以下操作,会收到令人困惑的错误消息:
错误:语法错误,意外的关系限定符输出,预期)在文件MixLogician中。第32行的dl Has(输出):-Has(输入),Begets(输入,输出)----^---------------------------------------1生成错误,评估中止
您可能还记得,我们最初从一个文件中加载了Has,它只是Datalog称之为“事实”的一个简单列表但现在我们正在动态地向它添加事实——我们一开始认为它是一个表,但现在它是一种奇怪的表/视图混合的东西。因此,SQL类比有点崩溃。
是的,考虑到生(x,x)这个事实可能会有点困惑,所以我们在这里做一种自我引用的无限陈述:“如果你有x,那么你有x,因为x生x。”但数据日志并不介意。
Begets(x,result):-Composite(result,first,second)、Has(first)、Begets(x,second)。Begets(x,result):-Composite(result,first,second)、Has(second)、Begets(x,first)。
在这里,我们说“复合成分”中的一种成分产生了该复合成分,但前提是你已经有了另一种成分。
这是我们掌握的第一条复杂规则,所以我将从更简单的例子中学习。这是我第一次尝试:
也就是说,“如果你有糖,酸橙皮生酸橙皮,如果你有酸橙皮,糖生酸橙皮。”
这实际上非常有效——但是,你知道,我们不想用代码来写。我们想从文件中加载这些事实,所以我们引入了复合关系:
这只是我们上次写的东西的重述,但现在它适用于我们复合关系中的任何东西。
但这种逻辑有一个微妙的问题。也就是说:酸橙的热情会产生酸橙的热情,但酸橙不会。而且我通常不会自己储存石灰皮,所以根据这个逻辑,即使我有石灰,我也不能使石灰热乎乎的。
所以这就是我们有点间接的原因:只要你还有糖,任何能产生酸橙热情的东西也能产生酸橙热饮。让我们看看“具体”形式的工作规则,没有复合关系:
贝吉斯(x,";莱姆热诚";):有(";糖";),生菜(x,";酸橙皮";)。贝吉斯(x,";莱姆热诚";):有(";酸橙皮";),生出(x,";糖";)。
我认为这更容易阅读,你可以想象在这里应用相同的复合替换来获得上面的“完整”规则。
贝吉斯(x,";莱姆热诚";):有(y),生(y,";糖";),生菜(x,";酸橙皮";)。贝吉斯(x,";莱姆热诚";):有(y),生(y,";酸橙皮";),生出(x,";糖";)。
我之所以提到这一点,是因为在我意识到这是不必要的之前,我实际上先写了这篇文章:因为有一条规则:Has(out):-Has(in),beggets(in,out)。,我们通过Has(";石灰皮";)来报道这个案例一点3.
这是一个非常琐碎的帮手关系——如果一种饮料需要某种成分,而我们没有这种成分,那么它就缺少了这种成分。这是我们第一个关系否定的例子,这是一个有趣的短语。
我们用它来声明我们能够混合的所有饮料——也就是说,所有不含任何缺失成分的饮料。
但Souffle否认了这一点:我们需要限制这种关系的范围。您可能会认为这是在尝试创建一个SQL表,其中包含另一个SQL表中不存在的所有值,这会很快耗尽磁盘空间。或者,如果你是蛋奶酥,我猜你的记忆力会耗尽?
我认为这为数据日志的本质提供了一个有趣的洞察:尽管我们编写规则就像我们在声明函数一样,但最终我认为所有关系都需要能够实现为一大堆元组。引擎可能能够优化实际实现,但它需要成为可能。我想。就像我说的,我对数据日志一无所知。
这种关系非常简单,我第一次尝试使用Mixologician来制作饮料时就添加了这种关系。它不仅仅是一个名字列表,而是我的食谱,过滤到我可以制作的食谱中——这样我就可以搜索特定的成分,如果我想做一些特别的东西的话。
但最后一条规则是最美味的。因此,我们来到这里的原因是:
.decl启用(缺少:配料、饮料:配方)启用(配料、饮料):-!不可购买的(配料),缺少的(饮料,产品),缺少的(饮料,产品),计数:{Missing(饮料,)}=count:{Begets(配料,产品),缺少的(饮料,产品)}。。输出可混合(文件名=";可混合";)。输出可混合配方(文件名=";可混合配方";,分隔符=";<;-";)。输出启用(文件名=";购物清单";,分隔符=";->;";)
我会花很多时间来讨论这条规则,所以我想得到答案。先把输出线让开。当我编写这个程序时,我也会经常抛出其他关系,作为调试东西的一种方式。它非常有用。
启用(配料、饮料):-!不可购买(配料)、缺失(饮料、产品)、缺失(饮料、产品)、计数:{Missing(饮料、产品)}=count:{Begets(配料、产品)、缺失(饮料、产品)}。
这个Unbuyable(配料)条款很琐碎——它的存在只是为了过滤掉我们不想在产品中看到的东西,比如酸橙皮。让我们暂时忽略这一点:
启用(成分,饮料):-Missing(饮料,out),Begets(成分,out),count:{Missing(饮料,)}=count:{Begets(成分,产品),Missing(饮料,产品)}。
在英语中:“如果饮料缺少某种成分可以产生的东西,如果饮料中唯一缺少的成分可以由成分产生,那么成分可以让你制作饮料。”
不,但这就是它的意思。它真的说,“如果饮料中缺少某种成分可以产生的东西,如果饮料中缺少的成分的数量等于饮料中缺少的成分的数量,那么成分可以让你制作饮料。”我们实际上是在比较集合的基数,而不是集合的相等性,但是由于一个集合是另一个集合的子集,所以它们可以具有相同基数的唯一方法是如果集合是相同的。4如果可以的话,我们会比较set equality,但据我所知,Souffle不能这么做。
希望这是有道理的。说到底,这是一个非常简单的表达。但我花了好几个小时才写完。真正地我想我花了好几个小时想出了一条规则。这是非常有趣和有教育意义的,但我觉得当你直接跳到最后的答案时,这段旅程的很多好处都消失了。
当然,但我想后退几步,谈谈我是如何来到这里的。我希望你和我一起踏上一段旅程,以那句话结束。但会从非常不同的事情开始。
看,当我第一次开始时,我没有在数据日志中思考。我在用“逻辑”思考我想写这样的东西:
我想说“给我看看我目前没有的成分,但是如果我有了它们,我就可以混合一种新的饮料。”这是我花了很长时间试图翻译成数据日志的逻辑表达式,但这是不可能的——没有“假设”运算符。也许我可以在序言中写些类似的东西?如前所述,我对逻辑编程一无所知。
所以不管怎样,我很快意识到,在数据日志中表达这一点是不可能的。所以我试着做一些简单的事情:只需找到缺少一种成分的食谱。忘记所有关于生儿育女或合成物的事;我们稍后再加进去。现在,你怎么才能找到“几乎”可以混合的食谱呢?
这是我在翻译标准一阶逻辑“唯一性”表达式时极其笨拙的尝试:
基本上我想说的是“饮料缺少了成分,它没有遗漏任何其他东西。”但这不起作用:我仍然在用“逻辑”思考,而不是用数据日志。
最终,在阅读文档时,我看到了计数的总和,并能够写下这句话——我的第一个表达实际上告诉了我一些有趣的事情:
错误:见证问题:聚合器以参数为基础';在file mixologician中,s的内部作用域在外部作用域中不接地使用。第53AlmostMixable(drink)行的dl:-count:{Missing(drink,)}=1--------------^--------------------------------------------1生成错误,评估中止
我发现我需要第一个缺失的(饮料)来“磨碎”变量——你明白了。当使用SQL聚合表达式时,会遇到相同类型的错误——引用在同一级别“不存在”的变量。我真的不知道该用什么词来清楚地谈论这件事。
总之,我一到那里,就想看看缺少的配料。但这是一个简单的改变——别再忽视这个变量了:
太棒了!这确实告诉我哪些饮料缺少一种成分。
但现在我们需要担心那些“制造”其他成分的成分。在这一点上,我所拥有的只是一个看起来像这样的关系:
基本上,我还没有Begets(x,x)规则——但我们会做到的。
启用(成分,饮料):-Missing(饮料,成分),count:{Missing(饮料,饮料)}=1。启用(in,drink):-make(in,missing),Enables(missing,drink)。
但这第二条规则并不能满足所有要求:它基本上是说“如果一种饮料缺少一种成分,那么获得任何制造这种成分的东西也可以让你制作这种饮料。”
这适用于一些简单的事情,比如“酸橙给你酸橙汁,现在你可以做玛格丽塔了。”但是,如果一种饮料缺少多种成分,而所有这些成分都可以通过获取一种新成分来制作,那该怎么办?这在Gimlet的实践中出现:假设你有杜松子酒和糖,但你缺少了莱姆汁和莱姆酒。你只需要买酸橙,但是这个小玩意儿缺少两种成分,所以它不会出现。
启用(成分,饮料):-Missing(饮料,成分),count:{Missing(饮料,饮料)}=1。启用(成分,饮料):-Missing(饮料,out),make(成分,out),count:{Missing(饮料,)}=count:{make(成分,产品),Missing(饮料,产品)}。
有一段时间我觉得这很管用。看起来是这样的
......