在家调酒已经从偶发的闲暇嗜好演化为许多人的长期爱好:用有限的酒柜材料尝试尽可能多的配方,是一件既实用又有趣的事情。Mixologician,这个把调酒与Datalog结合起来的小项目,正是为了解决这样一个问题:给定现有酒吧物品,怎样计算出能调制的鸡尾酒、以及购买哪一种额外成分能最大化新增可调酒单?将这个想法以Datalog实现,不仅带来了独特的可推理性,还展示了逻辑编程在现实问题中的优雅与力量。本文围绕Mixologician的理念、核心规则、实现细节与实战经验展开,带你一步步理解如何用声明式逻辑去描述调酒世界,并将日常的"想买什么才能做更多酒"的问题自动化、可解释化。 从灵感到实现的旅程始于一个很普通的场景:清点酒柜,发现只有几样常见材料,面对一本厚厚的配方书却只会调出少数几款鸡尾酒。问题的实质不是配方的复杂,而是信息的扩展性:许多配方要求的"成分"并不总是以原始形式出现。比如配方要求"lime juice",但酒柜里只有"lime"。
再比如"lime zest"和"lime cordial"之间可以通过其他成分(例如糖)相互转换或组合。传统的集合运算可以解决"我缺哪些成分"这个问题,但在存在"成分可以被派生或组合"时,如何在声明式规则中优雅地表达"我能把现有材料变成配方所需材料"并推断出最优采购建议?Datalog提供了一个天然的思路:用关系与规则来描述物品、配方与生成/组合路径,然后用推理引擎自动推导所有可达的结论。 系统的核心数据模型包括几类关系。第一类是Has,表示你当前"有"的材料,通常由一个文本文件自动加载。第二类是Needs,表示配方与所需成分的关系,这相当于平坦化的菜谱表格,每一行是"cocktail <- ingredient"的形式,便于Datalog读取。第三类关键关系是Begets,概念上表达"一个成分可以派生出另一个成分",例如"lime begets lime juice"。
Begets不仅用于直接的派生,还支持自反性与传递性,让推理引擎可以递归地计算更长的派生路径。第四类是Composite,用来描述"复合成分"的生产需要两个现成成分,例如"lime cordial"可能需要"lime zest"和"sugar"。通过这些基本关系,Mixologician能够把静态的配方表扩展成一张语义网络,进一步推导"如果补充A,则能得到一系列派生成分,从而满足某些配方"。 在实现细节上,Soufflé Datalog作为推理引擎承担了规则求值的工作。为保证可读性与类型安全,项目为Ingredient和Recipe定义了符号类型,并把输入数据通过简单的文本文件导入到对应的关系中。IsRecipe和IsIngredient是两个辅助关系,用于从Needs或Composite与Begets中推导出配方与成分的域,避免手工列举所有可能的字符串常量。
Begets关系的定义体现了两个关键设计思想:第一项规则让所有成分自反地"beget"自己,第二项规则保证Begets的传递性。这个小小的设计,使得后续规则能够把"拥有成分"拓展为"拥有能在推导路径上到达的所有成分"。 Has关系引入了一条规则:如果你有in并且in begets out,那么你也"有"out。简而言之,拥有一个原料并不止意味着能用它本身,还意味着可以生成或替代配方中出现的其他成分。而Composite关系的作用在于描述"需要两个材料才能得到一个复合材料"的场景,进而间接影响Begets关系。例如,lime zest加上sugar可以beget lime cordial,如果酒柜里有sugar,那么任何能beget lime zest的物品(包括lime)都可以通过两步被视为能beget lime cordial。
这一层间接推导正是Mixologician想要模型化的复杂逻辑:材料之间并非孤立,而是一张可组合、可派生的网络。 确定哪些鸡尾酒可调(Mixable)以及哪些成分是"值得购买"的(Enables)是Mixologician的两项重要输出。Mixable由一个简单的逻辑定义得到:如果一个配方的所有Needs都已被满足(即不存在Missing),那么它可调。Missing关系则表示某饮品需要某成分但你当前没有。更具挑战的是启用型建议:如何找出那些你没有但买了就能扩展你可调酒单的成分?直觉上可以想做"如果买了X,那么某些配方的缺失会被填补"的模拟,但Datalog里没有"假设"或"反事实"直接表达。解决方案是通过集合计数聚合来实现排序与集合相等的判定:判断一个待购成分能够补齐某个配方缺失的所有项,即该成分能beget配方所有缺少的产品。
具体地,使用count聚合比较配方缺失项数量与该成分能beget并且同时缺失项的数量,若相等则说明购买该成分后配方将不再缺项,从而被启用。这种巧妙的数值比较避免了复杂的存在量词或高阶逻辑,完全以关系算子与聚合完成需求。 在设计过程中,作者经历了不少试错,从最初想直接表达"如果我买了X,那么会变成可调"的直观逻辑语句,到逐步引入统计聚合、开集域绑定与自反提供规则(Begets(x,x)),最终形成可维护且准确的规则集合。关键的工程理解之一是Datalog需要"有域"(grounding),也就是所有变量必须能在某个层级被绑定到实际的常量或已有关系中,这促使作者实现IsIngredient、IsRecipe等辅助关系来给规则提供合理的定义域。另一个经验是将"或"型配方项(例如"london dry gin or new american gin")作为单一不可购买的合成成分处理,然后通过Begets关系把具体的可买成分映射到该"或"成分,这样既保持了配方语义的一致,又避免了在计算最优购买列表时造成重复计数或错误偏好。 数据采集与预处理是让系统实用化的重要环节。
作者选择从公开的鸡尾酒网站抓取配方,使用命令行工具如wget和pup快速抓取与解析HTML节点,再借助一段sed脚本把原始文本节点拼接成每行一个配方项的整洁形式。处理工作还包括去掉"garnish"等次要项,统一语言与表达(例如把"lime juice"与"lemon juice"根据语境规范化),以及将"or"表达展开成Begets关系并标注为Unbuyable以避免出现在购物建议里。尽管这些文本处理有时显得粗糙或手工化,但配方数据的质量对规则推理结果影响巨大,花时间清洗与规范化原始数据是值得的投入。 测试与可重复实验也是项目成功的重要保障。作者采用Cram等轻量化工具把命令行操作、数据更改与输出结果作为回归测试的一部分。通过在不同的"酒柜状态"下运行脚本并比对输出,能快速发现规则改动带来的意外影响。
更重要的是,测试促进了规则的可解释性:当某个新用例没有得到预期结果时,通过输出中间关系(如Begets、Missing)可以一步步定位逻辑错误,从而调整Begets的自反性、Composite处理或聚合表达式的边界条件。 Mixologician的实践给出若干有趣的洞见。首先,声明式逻辑非常适合描述"规则多、例外多、组合关系复杂"的问题域。调酒世界对于替代、派生与配对关系的重视,天然契合Datalog的关系运算与递归推导。其次,把实际世界问题转换为关系与规则时,合理划分关系、明确边界与引入辅助域(例如IsIngredient)能极大地简化规则的书写与推理效率。最后,工程实践表明自动化的数据采集、管道式预处理与持续化测试是把学术原理转化为日常可用工具的关键。
对调酒爱好者与工程实践者来说,Mixologician也指明了许多可扩展方向。可以把更多的来源纳入配方库,比如不同地域的配方集合,或社区贡献的本地化改良版,以增强推荐的多样性。可以把成本、保质期或瓶装容量等因素纳入启用优先级评估中,让购买建议变得更贴近现实决策。也可以把配方步骤、用量与配比信息加入模型,结合简单的度量学习或排名模型,把"能做多少款酒"换成"能做多少款你偏好的酒"或"购买成本最低但覆盖品类最多"的策略。此外,将程序包装成交互式网页或移动应用能让更广泛的爱好者直接受益,并提供可视化的材料网络图,帮助理解某个购买决策如何影响整张配方网络。 把调酒和编程结合起来并不只是冷冰冰的工程练习。
对于许多动手做饮品的人来说,Mixologician是一种新颖的创作工具。它能让你在清点酒柜时产生灵感:看到"若买了这瓶,能做多少新酒"的即时反馈,会促使你尝试新的口味组合或购买更具多功能性的材料。对于注重经济与实用性的家庭酒吧而言,该工具可以最大化每一项开支的性价比,减少因买了只为做某一款酒而闲置的高价利口酒的浪费。 技术上,Mixologician展示了如何用有限的规则组合表达丰富的现实世界概念:自反性、传递性、复合生成、可购买性约束以及聚合计数等组成了一个高度可解释的推理体系。更重要的是,Datalog本身对关系的直观表达,使得规则库在长期维护中更容易被修改与扩展,而不是陷入一堆难以理解的程序逻辑分支。 如果你对试验感兴趣,可以从自己的酒柜做起:把现有材料列成文本文件,把常用的配方用"配方 <- 成分"格式录入,补充简单的Begets规则(例如水果到果汁、果皮到zest、不同烈酒之间的泛化关系),再尝试写一个简单的Datalog规则来推断Mixable与Enables。
随后逐步加入Composite、Unbuyable与更复杂的Begets传递规则。借助开源的Soufflé或其他Datalog实现,你很快能把零碎的厨房库存变成一个智能的调酒助理。 Mixologician既是一个有趣的工程小品,也是一个关于如何用声明式逻辑建模真实世界问题的优秀范例。它告诉我们,很多生活中的"搜索与决策"问题,实际上可以通过定义清晰的关系与规则得到优雅而可解释的解决方案。对懂得调酒之道的人来说,它把无穷的材料与配方转化为可以计算的语义网络;对喜欢编程的人来说,它把看似随意的生活习惯转化为有意义的数据与规则。无论身份如何,读者都可以从中获得启发:在现实世界里,结构化的知识加上合适的推理工具,就能把混乱的选择变成清晰的行动建议,而一杯好酒,往往就是如此一步步被理性而有趣地酿成的。
。