Quoting Difficulties

Quoting Difficulties / 引用难题

In my last post I discussed DSLs for database querying in Clojure. These typically take the form of data structures. I also discussed how some query languages, like SPARQL and Datomic, use variables in their queries, and that these appear in Clojure as symbols. That post also demonstrated using quoting to embed symbols easily into a structure, and unquoting to use values inside those same structures. Some of it got messy.

在上一篇文章中,我讨论了 Clojure 中用于数据库查询的 DSL(领域特定语言)。它们通常以数据结构的形式存在。我还讨论了某些查询语言(如 SPARQL 和 Datomic)如何在查询中使用变量,以及这些变量在 Clojure 中如何表现为符号(symbols)。那篇文章还演示了如何使用引用(quoting)将符号轻松嵌入到结构中,以及如何使用反引用(unquoting)在这些结构内部使用值。其中一些过程变得有些混乱。

Symbol Reuse / 符号重用

A colleague was recently trying to build SPARQL queries using Flint. This is a library that allows SPARQL queries that look very similar to Datomic queries. He was trying to programmatically build query fragments that could be appended to each other to form a complete query. Each fragment was generated by functions that returned a small structure that could be added into the query.

最近,一位同事尝试使用 Flint 构建 SPARQL 查询。这是一个允许 SPARQL 查询看起来非常类似于 Datomic 查询的库。他试图以编程方式构建查询片段,并将它们拼接在一起以形成完整的查询。每个片段都由函数生成,这些函数返回一个可以添加到查询中的小结构。

In most cases, he could use quoting to return his structure. For instance, the following fragment might be used to find the name of a person who had changed an entity: [[entity :data/modifiedBy ?person] [?person :data/firstName ?name]] This is not the final form though, since he wanted to both pass in a value for entity, while also quoting the symbols in his structure. Using the techniques from the last post, this is relatively straightforward: (defn modifier-name [entity] [[entity :data/modifiedBy '?person] ['?person :data/firstName '?name]])

在大多数情况下,他可以使用引用来返回他的结构。例如,以下片段可用于查找更改了某个实体的人的姓名: [[entity :data/modifiedBy ?person] [?person :data/firstName ?name]] 但这还不是最终形式,因为他既想传入 entity 的值,又想引用结构中的符号。使用上一篇文章中的技术,这相对简单: (defn modifier-name [entity] [[entity :data/modifiedBy '?person] ['?person :data/firstName '?name]])

However, there are occasions where the entity might be modified more than once, and so multiple names should be returned. This will need a configurable name variable: (defn modifier-name [entity name-var] [[entity :data/modifiedBy '?person] ['?person :data/firstName name-var]]) This would let a developer call something like: (concat (modifier-name '?entity '?name1) (modifier-name '?entity '?name2)) (note: There are overlaps between what ?name1 and ?name2 will bind to. This is just for illustration.)

然而,有时实体可能会被多次修改,因此需要返回多个姓名。这就需要一个可配置的姓名变量: (defn modifier-name [entity name-var] [[entity :data/modifiedBy '?person] ['?person :data/firstName name-var]]) 这将允许开发人员进行如下调用: (concat (modifier-name '?entity '?name1) (modifier-name '?entity '?name2)) (注意:?name1 和 ?name2 绑定的内容之间存在重叠。这仅用于说明。)

Autogensym / 自动生成符号

Unfortunately, this has a bug. In both cases, the ?person variable is used, meaning that both ?name1 and ?name2 will always be bound to the same values. One way to address this is to generate a new symbol for the query. Since we’re been using quoting, then a common way to generate symbols is to use a syntactic feature called an autogensym inside a quote. This uses a symbol name with a trailing # character: (defn modifier-name [entity name-var] [[~entity :data/modifiedBy ?person#] [?person# :data/firstName ~name-var]])`

不幸的是,这有一个 bug。在这两种情况下,都使用了 ?person 变量,这意味着 ?name1 和 ?name2 将始终绑定到相同的值。解决这个问题的一种方法是为查询生成一个新的符号。由于我们一直在使用引用,因此生成符号的一种常见方法是在引用内部使用一种称为“自动生成符号”(autogensym)的语法特性。它使用带有后缀 # 字符的符号名称: (defn modifier-name [entity name-var] [[~entity :data/modifiedBy ?person#] [?person# :data/firstName ~name-var]])`

This version is using syntax quoting, and embedding entity and name-var, as discussed in the previous post. However, this version has a bug too. The appearance of ?person# in the code tells the Clojure reader to generate a new symbol. user=> (modifier-name '?entity '?name) [[?entity :data/modifiedBy ?person__2__auto__] [?person__2__auto__ :data/firstName ?name1]]

此版本使用了语法引用(syntax quoting),并嵌入了 entity 和 name-var,正如上一篇文章所讨论的那样。然而,这个版本也有一个 bug。代码中 ?person# 的出现告诉 Clojure 读取器生成一个新的符号。 user=> (modifier-name '?entity '?name) [[?entity :data/modifiedBy ?person__2__auto__] [?person__2__auto__ :data/firstName ?name1]]

Each new use of this ?person# expression (in a new context) should result in a new symbol. However, this symbol gets reused when the function gets called again. user=> (concat (modifier-name '?entity '?name1) (modifier-name '?entity '?name2)) ([?entity :data/modifiedBy ?person__2__auto__] [?person__2__auto__ :data/firstName ?name1] [?entity :data/modifiedBy ?person__2__auto__] [?person__2__auto__ :data/firstName ?name2])

每次使用此 ?person# 表达式(在新的上下文中)都应该产生一个新的符号。然而,当函数再次被调用时,这个符号会被重用。 user=> (concat (modifier-name '?entity '?name1) (modifier-name '?entity '?name2)) ([?entity :data/modifiedBy ?person__2__auto__] [?person__2__auto__ :data/firstName ?name1] [?entity :data/modifiedBy ?person__2__auto__] [?person__2__auto__ :data/firstName ?name2])

The symbol ?person__2__auto__ was returned from both calls, because the generation actually occurred when the function was read, not when it was executed. This is the same issue that was discussed in an Ask Clojure Question. Syntax quoting and autogensyms are most often used in macros, and the scope of a generated value is typically restricted so that any generated symbols cannot interact with each other.

两次调用都返回了符号 ?person__2__auto__,因为生成过程实际上是在函数被读取时发生的,而不是在执行时。这与 Ask Clojure 问题中讨论的问题相同。语法引用和自动生成符号最常用于宏中,生成值的作用域通常受到限制,以确保任何生成的符号都不会相互干扰。

The case discussed in that Clojure question was when a macro was recursive. In that case, the symbols generated during recursion were all the same, since they all shared scope. Our query is not using recursion, but instead it is capturing the name of this symbol and returning it to the calling scope. This means that the scope of the generated name is extended to the calling context, allowing it to interfere with other generated names in that context. i.e. the scope “escaped”.

那个 Clojure 问题中讨论的情况是宏递归时。在这种情况下,递归期间生成的符号都是相同的,因为它们共享作用域。我们的查询没有使用递归,而是捕获了这个符号的名称并将其返回给调用作用域。这意味着生成名称的作用域被扩展到了调用上下文,从而允许它干扰该上下文中的其他生成名称。即作用域“逃逸”了。

In other words, despite autogensyms being common when quoting expressions (most commonly in macros), they are not appropriate for anything that can escape the current context.

换句话说,尽管自动生成符号在引用表达式时很常见(最常见于宏中),但它们并不适用于任何可能逃逸出当前上下文的内容。

Addressing the issue / 解决问题

One solution to this is to generate a symbol on each execution of the function. This can be done manually, rather than using the autogensym syntax: (defn modifier-name [entity name-var] (let [?person (gensym "?person")] [[entity :data/modifiedBy ?person] [?person :data/firstName name-var]])) This creates a new value every time.

解决此问题的一种方法是在每次执行函数时生成一个符号。这可以手动完成,而不是使用自动生成符号语法: (defn modifier-name [entity name-var] (let [?person (gensym "?person")] [[entity :data/modifiedBy ?person] [?person :data/firstName name-var]])) 这每次都会创建一个新值。

Recap / 回顾

Using functions to generate fragments of queries can result in conflicting fragments, particularly in graph languages that have variables in their syntax. In these cases, a new variable is needed for each fragment. Clojure has a facility for creating new variable names easily, called “autogensym”. However, the new name is only generated when the code is read, meaning that any function using this construct will always return the same symbol. “Autogensym” can be helpful, but only if the context of the generated symbol can never overlap with the context of another call to the same autogensym.

使用函数生成查询片段可能会导致片段冲突,特别是在语法中包含变量的图查询语言中。在这些情况下,每个片段都需要一个新的变量。Clojure 提供了一种轻松创建新变量名的工具,称为“自动生成符号”。然而,新名称仅在代码被读取时生成,这意味着任何使用此结构的函数都将始终返回相同的符号。“自动生成符号”可能很有用,但前提是生成符号的上下文永远不会与对同一自动生成符号的另一次调用的上下文重叠。

Query Mess / 查询混乱

In the first post I mentioned that macros can simplify a query DSL. The next post will demonstrate this.

在第一篇文章中,我提到宏可以简化查询 DSL。下一篇文章将演示这一点。