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
函数尝试从 f
和 g
中提取 values 时,有什么都不存在:变量存在但它们是空的。
P.S.
上面有一个例外。如果您有 let
表格或类似表格,则不涉及 var
:
(let [x 5
y (* 2 x) ]
y)
;=> 10
在 let
形式中,不存在 var。相反,编译器在符号与其关联值之间建立直接联系;即 x => 5
和 y => 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。同样,这只是为了示例,我不会推荐它。
我喜欢我的代码有一个 "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)))
并得到相同的结果。
请 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
函数尝试从 f
和 g
中提取 values 时,有什么都不存在:变量存在但它们是空的。
P.S.
上面有一个例外。如果您有 let
表格或类似表格,则不涉及 var
:
(let [x 5
y (* 2 x) ]
y)
;=> 10
在 let
形式中,不存在 var。相反,编译器在符号与其关联值之间建立直接联系;即 x => 5
和 y => 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。同样,这只是为了示例,我不会推荐它。