Clojure中的前向声明有哪些限制?为什么在此示例中不能使用comp?

时间:2018-08-13 19:48:47

标签: clojure forward-declaration declare

我喜欢我的代码具有“自上而下”的结构,这意味着我想做的事情与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?我怎么知道何时必须严格定义?

2 个答案:

答案 0 :(得分:6)

如果您利用Clojure(记录不充分)的var行为,就可以实现自己的目标:

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

(mycomp 10) => 45

请注意,语法#'f只是转换为(var f)的简写(技术上是“阅读器宏”)。因此,您可以直接编写以下代码:

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

并获得相同的结果。

see this answer详细了解Clojure符号(例如f)与该符号所指向的(匿名)Clojure var之间的相互作用(大部分是隐藏的),即{ {1}}或#'f。然后,该var然后指向一个值(例如您的函数(var f)

在编写(fn [x] (* x 3))之类的表达式时,会出现两步间接访问。首先,对符号(f 10)进行“求值”以找到相关的变量,然后对变量{进行求值”以查找相关的函数。大多数Clojure用户并没有真正意识到此两步过程的存在,并且几乎所有时间我们都可以假装符号f与函数值f之间存在直接联系。

原始代码无法正常工作的具体原因是

(fn [x] (* x 3))

创建2个“空”变量。就像(declare f g) 在符号(def x)和一个空var之间创建关联一样,x也是如此。因此,当declare函数尝试从compf提取 时,则不存在任何内容:变量存在但它们是空的。


P.S。

以上是一个例外。如果您使用g表格或类似表格,则不涉及let

var

(let [x 5 y (* 2 x) ] y) ;=> 10 格式中,不存在var。相反,编译器在符号及其关联值之间建立直接连接。即letx => 5

答案 1 :(得分:3)

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

但是...只是为了开怀大笑,并演示Clojure宏的可能,这是comp的另一种(更差的)实现,可用于您的首选语法,而无需直接处理vars:

(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))))

之所以可行,是因为您的(未绑定)函数没有作为值传递给另一个函数;在编译过程中,宏将“就地”展开,就像您是手工编写合成函数一样,如第三个示例中所示。

(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。再说一次,这只是为了举例,我不建议这样做。