6

鉴于这可以按我的预期工作:

(do
  (println (resolve 'a)) ; nil 
  (def a "a")
  (println (resolve 'a))) ; #'user/a

我想了解为什么这不会:

(future
  (println (resolve 'b)) ; #'user/b (shouldn't it be still undefined at this point?)
  (def b "b")
  (println (resolve 'b))) ; #'user/b

我也想知道这是否是一个合适的解决方案(不完全解决同样的问题,但在我的上下文中做同样的工作):

(def c (atom nil))
(future
  (println @c) ; nil
  (reset! c "c")
  (println @c)) ; c
4

1 回答 1

7

这种行为是def表单编译方式的结果。

请注意,在任何情况下,作为一种风格问题,使用def不在顶级(或可能在顶级内 - 有关此案例的更多评论,请参见下文)的表单都是不受欢迎的。let另一方面,使用 Atom 的代码片段很好——如果它可以满足您的需求,就没有理由不使用它。

def故事:

  1. def表格汇编:

    def遇到表单时,编译器会在当前命名空间中创建一个适当名称的Vardef(通过使用命名空间限定符号作为名称参数来尝试在当前命名空间之外的 Var 会def导致异常)。Var 起初是未绑定的,并且在def实际执行之前一直保持未绑定;对于顶级的def,那将是马上,但对于def隐藏在函数体内(或在let表单内——见下文),那将是函数被调用的时候:

    ;;; in the user namespace:
    
    (defn foo []
      (def bar "asdf")
     :done)
    ; => #'user/foo
    
    bar
    ; => #<Unbound Unbound: #'user/bar>
    
    ;;; let's change the namespace and call foo:
    
    (ns some.ns)
    (user/foo)
    ; => :done
    
    bar
    ; exception, the bar Var was created in the user namespace!
    
    user/bar
    ; => "asdf"
    ; the Var's namespace is fixed at compile time
    
  2. 第一个例子——do形式为:

    顶级dos 被视为它们的内容在do发生的地方被拼接到代码流中。因此,如果您(do (println ...) (def ...) (println ...))在 REPL 处键入,则相当于键入第一个println表达式,然后是def,然后是第二个println表达式(除了 REPL 只产生一个新提示)。

  3. 第二个例子——用future

    (future ...)扩展到接近(future-call (fn [] ...)). 如果...包含一个def表单,它将按照我们上面看到的方式进行编译。当匿名函数在其自己的线程上执行时,Var 将被创建,因此resolve将能够找到它。

  4. 作为旁注,让我们看一下类似的片段及其输出:

    (let []
      (println (resolve 'c)) 
      (def c "c") 
      (println (resolve 'c)))
    ; #'user/c
    ; #'user/c
    ; => nil
    

    原因和以前一样,let先编译,然后作为一个整体执行。这是在使用内部带有定义的顶级let表单时应该牢记的一点——只要没有副作用代码与定义混合,通常就可以了;否则必须格外小心。

于 2012-08-21T18:16:56.460 回答