如何使Clojure在编译时计算常量局部表达式

如何使Clojure在编译时计算常量局部表达式,clojure,Clojure,使用4Clojure,我可以将卡片值从字符转换为数字: (fn [ch] (or ({\A 1} ch) ((zipmap "TJQK" (iterate inc 10)) ch) (- (int ch) (int \0)))) zipmap表达式在每次调用时都会求值,始终生成{\K 13、\Q 12、\J 11、\T 10} 我们如何让编译器只计算一次呢 经过深思熟虑,我想出了一个好主意 (defmacro constant [exp] (eval exp))

使用4Clojure,我可以将卡片值从字符转换为数字:

 (fn [ch]
   (or
    ({\A 1} ch)
    ((zipmap "TJQK" (iterate inc 10)) ch)
    (- (int ch) (int \0))))
zipmap表达式在每次调用时都会求值,始终生成{\K 13、\Q 12、\J 11、\T 10}

我们如何让编译器只计算一次呢

经过深思熟虑,我想出了一个好主意

(defmacro constant [exp] (eval exp))
。。。将zipmap调用包装为:

(constant (zipmap "TJQK" (iterate inc 10)))
我认为这相当于

(eval '(zipmap "TJQK" (iterate inc 10)))
。。。但在没有报价的情况下,不要进行评估:

(eval (zipmap "TJQK" (iterate inc 10)))
欢迎更正、评论和改进

常量宏适用于这种特殊情况,因为正在计算的表单只有解析为clojure.core和编译时文本中函数的符号。在其他情况下,您可能会遇到符号解析问题,例如从函数参数计算用于匿名函数的局部常量

更通用的方法是将对zipmap的调用移到fn表单之外。一个选项是使用def将zipmap调用的结果存储在var中,如def my const zipmap TJQK iterate inc 10中所示

然而,在这种情况下,如果您正在创建一个匿名函数,那么创建一个全局可访问的var可能会有些过分。因此,我建议将fn放入捕获常量值的let绑定中:

 (let [face-cards (zipmap "TJQK" (iterate inc 10))]
   (fn [ch]
     (or
       ({\A 1} ch)
       (face-cards ch)
       (- (int ch) (int \0)))))

编辑:正如注释中指出的,这个解决方案将在加载时计算常量值,而不是在编译时。我很难想出一个重要的场景,但值得指出的是,它的语义与您的方法稍有不同。

eval也在运行时调用,所以我认为这并不能改善情况。没关系,我错误地认为eval被转义了。不过,我认为我的答案是一个更通用的解决方案,它将处理编译时文本以外的事情。在大约21-22分钟的时间里,他创建了一个自定义分析器过程,该过程用其返回值替换常量节点。但正如他所说,出于性能方面的原因,这可能不值得去做,因为这不太可能带来巨大的好处,但作为一种学习体验,它非常有趣。我原以为上面的常量宏将在宏展开时展开,并用文本结果替换zipmap调用。这是错的吗?不,你是对的-见我在帖子上的新评论。常量宏在某些情况下会起作用。我想澄清一点,我相信let-over lambda和def都将在加载时而不是编译时进行计算。它们以惯用的方式实现了唯一的目标,但不是在编译时实现的。@Alex。非常感谢。恐怕还有更多的问题:为什么让我们帮忙?是否不是每次输入let表单时都对绑定表达式进行理论计算?那么,作为常量,绑定表达式只计算一次,这是编译器的特性,而不是语言的特性吗?那么为什么编译器没有注意到内联表达式是常量?@Thumbnail是的,每次输入let时都会计算表达式。我们的想法是只进入它一次。如果您将这个让渡lambda构造放在def中,或者将其放在顶层,并将内部fn更改为defn,那么只需对其求值一次。如果它位于另一个函数内部,即生成内部函数作为返回值的函数,则每次调用外部函数时都会对其进行评估,如图取决于粘贴到生成器的参数时所需。如果您不希望出现这种行为,例如,将let移出外部fn。