Clojure 中前向声明的限制是什么?为什么我不能在这个例子中使用 comp?

What are the limitations of forward declaring in Clojure? Why can't I use comp in this example?

我喜欢我的代码有一个 "top-down" 结构,这意味着我想做与 Clojure 中自然的事情完全相反的事情:函数在使用之前定义。不过,这应该不是问题,因为理论上我可以先 declare 我的所有功能,然后继续享受生活。但实际上 declare 似乎无法解决所有问题,我想了解以下代码不起作用的确切原因。

我有两个函数,我想通过组合这两个函数来定义第三个函数。以下三段代码实现了这一点:

1

(defn f [x] (* x 3))
(defn g [x] (+ x 5))
(defn mycomp [x] (f (g x)))
(println (mycomp 10))

2

(defn f [x] (* x 3))
(defn g [x] (+ x 5))
(def mycomp (comp f g))

3

(declare f g)
(defn mycomp [x] (f (g x)))
(defn f [x] (* x 3))
(defn g [x] (+ x 5))

但我真正想写的是

(declare f g)
(def mycomp (comp f g))
(defn f [x] (* x 3))
(defn g [x] (+ x 5))

这给了我

Exception in thread "main" java.lang.IllegalStateException: Attempting to call unbound fn: #'user/g,

这意味着前向声明适用于许多情况,但在某些情况下我不能只 declare 我的所有函数并以我喜欢的任何方式和顺序编写代码。这个错误的原因是什么?前向声明真正允许我做什么,在什么情况下我必须已经定义了函数,例如在这种情况下使用 comp?我怎么知道什么时候定义是绝对必要的?

如果您利用 Clojure 的(记录不完整的)var 行为,您可以实现您的目标:

(declare f g)
(def mycomp (comp #'f #'g))
(defn f [x] (* x 3))
(defn g [x] (+ x 5))

(mycomp 10) => 45

请注意,语法 #'f 只是 shorthand(技术上是 "reader macro")转换为 (var f)。所以你可以直接这样写:

(def mycomp (comp (var f) (var g)))

并得到相同的结果。

获取有关 Clojure 符号(例如 f)与该符号指向的(匿名)Clojure var 之间(大部分是隐藏的)交互的更详细的答案,即#'f(var f)。然后,var 又指向一个值(例如您的函数 (fn [x] (* x 3)).

当你写一个像 (f 10) 这样的表达式时,一个两步间接 在起作用。首先,符号f是"evaluated"找到关联的var,然后var是"evaluated"找到关联的函数。大多数 Clojure 用户并没有真正意识到这个两步过程的存在,几乎所有时候我们都可以假装符号 f 和函数值 (fn [x] (* x 3)) 之间存在直接联系。

您的原始代码不起作用的具体原因是

(declare f g)

创建 2 "empty" 个变量。正如 (def x) 在符号 x 和空变量之间创建关联一样,这就是您的 declare 所做的。因此,当 comp 函数尝试从 fg 中提取 values 时,有什么都不存在:变量存在但它们是空的。


P.S.

上面有一个例外。如果您有 let 表格或类似表格,则不涉及 var

(let [x 5
      y (* 2 x) ]
  y)  

;=> 10

let 形式中,不存在 var。相反,编译器在符号与其关联值之间建立直接联系;即 x => 5y => 10.

我认为 Alan 的回答很好地解决了您的问题。您的第三个示例之所以有效,是因为您没有 将函数作为参数 传递给 mycomp。我会重新考虑尝试按 "reverse" 顺序定义事物,因为它不符合基本语言设计,需要更多代码,并且其他人可能更难理解。

但是...只是为了开怀大笑并演示 Clojure 宏的可能性,这里是 comp 的另一种(更糟糕的)实现,它适用于您喜欢的语法,无需直接处理变量:

(defn- comp-fn-arity [variadic? args f & fs] ;; emits a ([x] (f (g x)) like form
  (let [args-vec (if variadic?
                   (into (vec (butlast args)) ['& (last args)])
                   (apply vector args))
        body (reduce #(list %2 %1)
                     (if variadic?
                       (apply list 'apply (last fs) args)
                       (apply list (last fs) args))
                     (reverse (cons f (butlast fs))))]
    `(~args-vec ~body)))

(defmacro momp
  ([] identity)
  ([f] f)
  ([f & fs]
   (let [num-arities 5
         args-syms (repeatedly num-arities gensym)]
     `(fn ~@(map #(apply comp-fn-arity (= % (dec num-arities)) (take % args-syms) f fs)
                 (range num-arities))))))

这将发出类似于 comp 的实现:

(macroexpand '(momp f g))
=>
(fn*
 ([] (f (g)))
 ([G__1713] (f (g G__1713)))
 ([G__1713 G__1714] (f (g G__1713 G__1714)))
 ([G__1713 G__1714 G__1715] (f (g G__1713 G__1714 G__1715)))
 ([G__1713 G__1714 G__1715 & G__1716] (f (apply g G__1713 G__1714 G__1715 G__1716))))

这是有效的,因为您的(未绑定的)函数没有作为值传递给另一个函数;在编译过程中,宏扩展 "in place" 就像您手动编写组合函数一样,如您的第三个示例。

(declare f g)
(def mycomp (momp f g))
(defn f [x] (* x 3))
(defn g [x] (+ x 5))
(mycomp 10) ;; => 45
(apply (momp vec reverse list) (range 10)) ;; => [9 8 7 6 5 4 3 2 1 0]

这在某些其他情况下不起作用,例如((momp - dec) 1) 失败,因为 dec 得到 内联 并且没有 0-arg arity 来匹配宏的 0-arg arity。同样,这只是为了示例,我不会推荐它。