【问题标题】:When to use a Var instead of a function?何时使用 Var 而不是函数?
【发布时间】:2017-01-25 19:20:24
【问题描述】:

我正在阅读Web Development with Clojure 这本书,它告诉我将处理程序(定义如下)作为 Var 对象而不是函数本身传递,因为函数可以动态更改(这就是 wrap-reload 所做的)。

书上说:

"请注意,我们必须从处理程序中创建一个 var 才能使用此中间件 去工作。这是必要的,以确保 Var 对象包含当前 返回处理函数。如果我们改用处理程序,那么应用程序将 只看到函数的原始值,变化不会体现出来。” 我不太明白这是什么意思,vars 和 c 指针类似吗?

(ns ring-app.core
  (:require [ring.adapter.jetty :as jetty]
            [ring.util.response :as response]
            [ring.middleware.reload :refer [wrap-reload]]))

(defn handler [request]
  (response/response
   (str "<html>/<body> your IP is: " (:remote-addr request)
        "</body></html>")))

(defn wrap-nocache [handler]
  (fn [request]
    (-> request
        handler
        (assoc-in [:headers "Pragma"] "no-cache"))))

这是处理程序调用:

(defn -main []
  (jetty/run-jetty
   (wrap-reload (wrap-nocache  (var handler)))
   {:port 3001
    :join? false}))

【问题讨论】:

    标签: web clojure var


    【解决方案1】:

    是的,Clojure 中的 Var 类似于 C 指针。这没有很好的记录。

    假设你创建一个函数fred如下:

    (defn fred [x] (+ x 1))
    

    这里实际上有 3 件事。首先,fred 是一个符号。符号 fred(无引号)和关键字 :fred(由前导 : 字符标记)和字符串 "fred"(由两端的双引号标记)之间存在差异。对于 Clojure,它们每个都由 4 个字符组成;即关键字的冒号和字符串的双引号都不包含在它们的长度或组成中:

    > (name 'fred)
    "fred"
    > (name :fred)
    "fred"
    > (name "fred")
    "fred"
    

    唯一的区别是它们的解释方式。字符串旨在表示任何类型的用户数据。关键字旨在以可读的形式表示程序的控制信息(与 1=left、2=right 之类的“幻数”相反,我们只使用关键字 :left:right

    符号意味着指向事物,就像在 Java 或 C 中一样。如果我们说

    (let [x 1
          y (+ x 1) ]
      (println y))
    ;=> 2
    

    然后x指向值1,y指向值2,我们看到打印的结果。

    (def ...) 表单引入了 invisible 第三个元素,即 var。所以如果我们说

    (def wilma 3)
    

    我们现在要考虑 3 个对象。 wilma 是一个符号,它指向var,而var 又指向值3。当我们的程序遇到符号wilma 时,求值 以找到var。同样,var 被 求值 以产生值 3。所以它就像 C 中指针的 2 级间接。因为符号 和 strong> var 是“自动评估”的,这会自动且不可见地发生,您不必考虑 var(事实上,大多数人并没有真正意识到甚至存在不可见的中间步骤)。

    对于我们上面的函数fred,存在类似的情况,除了var指向匿名函数(fn [x] (+ x 1))而不是值3,就像wilma一样。

    我们可以“短路” var 的自动评估,例如:

    > (var wilma)
    #'clj.core/wilma
    

    > #'wilma
    #'clj.core/wilma
    

    阅读器宏#'(磅引号)是调用(var ...) 特殊形式的简写方式。请记住,像 var 这样的特殊形式是像 ifdef 这样的内置编译器,并且与常规函数相同。 var 特殊形式返回附加到符号 wilma 的 Var 对象。 clojure REPL 使用相同的速记打印 Var 对象,因此两个结果看起来相同。

    一旦我们有了Var 对象,自动评估就会被禁用:

    > (println (var wilma))
    #'clj.core/wilma
    

    如果我们想得到wilma指向的值,我们需要使用var-get

    > (var-get (var wilma))
    3
    > (var-get    #'wilma)
    3
    

    同样的事情也适用于弗雷德:

    > (var-get #'fred)
    #object[clj.core$fred 0x599adf07 "clj.core$fred@599adf07"]
    > (var-get (var fred))
    #object[clj.core$fred 0x599adf07 "clj.core$fred@599adf07"]
    

    #object[clj.core$fred ...] 是 Clojure 将函数对象表示为字符串的方式。

    对于网络服务器,它可以通过var? 函数或其他方式判断提供的值是处理函数还是指向处理函数的var。

    如果你输入如下内容:

    (jetty/run-jetty handler)
    

    双重自动评估将产生处理函数对象,该对象被传递给run-jetty。相反,如果您键入:

    (jetty/run-jetty (var handler))
    

    然后将指向处理函数对象的Var 传递给run-jetty。然后,run-jetty 必须使用if 语句或等效语句来确定它收到了什么,如果它收到的是Var 而不是函数,则调用(var-get ...)。因此,每次通过(var-get ...) 都会返回Var 当前指向的对象。所以,Var 就像 C 中的全局指针或 Java 中的全局“引用”变量。

    如果你将一个函数对象传递给run-jetty,它会保存一个指向函数对象的“本地指针”,而外界无法更改本地指针所指的内容。

    您可以在此处找到更多详细信息:


    更新

    正如OlegTheCat 所指出的,Clojure 对指向 Clojure 函数的 Var 对象还有另一个技巧。考虑一个简单的函数:

    (defn add-3 [x] (+ x 3))
    ; `add-3` is a global symbol that points to
    ;     a Var object, that points to
    ;         a function object.
    
    (dotest
      (let [add-3-fn  add-3           ; a local pointer to the fn object
            add-3-var (var add-3)]    ; a local pointer to the Var object
        (is= 42 (add-3 39))           ; double deref from global symbol to fn object
        (is= 42 (add-3-fn 39))        ; single deref from local  symbol to fn object
        (is= 42 (add-3-var 39)))      ; use the Var object as a function 
                                      ;   => SILENT deref to fn object
    

    如果我们将 Var 对象视为函数,Clojure 会将其静默解引用到函数对象中,然后使用提供的参数调用该函数对象。所以我们看到add-3add-3-fnadd-3-var 这三个都可以工作。这就是 Jetty 中正在发生的事情。它永远不会意识到你给了它一个 Var 对象而不是一个函数,但是 Clojure 神奇地修补了这种不匹配而不告诉你。

    侧边栏:请注意,这仅适用于我们的“码头”实际上是 Clojure 包装器代码 ring.adapter.jetty,而不是实际的 Java webserver Jetty。如果你试图依靠这个技巧 实际的 Java 函数而不是 Clojure 包装器,它会失败。实际上,您必须使用像 proxy 这样的 Clojure 包装器才能将 Clojure 函数传递给 Java 代码。

    如果你将 Var 对象用作函数以外的任何东西,你就没有这样的守护天使来拯救你:

      (let [wilma-long  wilma         ; a local pointer to the long object
            wilma-var   (var wilma)]  ; a local pointer to the Var object
        (is (int? wilma-long))        ; it is a Long integer object
        (is (var? wilma-var))         ; it is a Var object
    
        (is= 4 (inc wilma))          ; double deref from global symbol to Long object
        (is= 4 (inc wilma-long))     ; single deref from local  symbol to Long object
        (throws? (inc wilma-var))))  ; Var object used as arg => WILL NOT deref to Long object
    

    所以,如果你期待一个函数并且有人给你一个指向函数的 Var 对象,你没问题,因为 Clojure 默默地解决了这个问题。如果您期待函数以外的任何东西,而有人给了您一个指向该东西的 Var 对象,那么您只能靠自己了。

    考虑这个辅助函数:

    (defn unvar
      "When passed a clojure var-object, returns the referenced value (via deref/var-get);
      else returns arg unchanged. Idempotent to multiple calls."
      [value-or-var]
      (if (var? value-or-var)
        (deref value-or-var) ; or var-get
        value-or-var))
    

    现在你可以安全地使用你得到的东西了:

    (is= 42 (+ 39 (unvar wilma))
            (+ 39 (unvar wilma-long))
            (+ 39 (unvar wilma-var)))
    

    附录

    请注意,三个二元性可能会混淆问题:

    • var-getderef 都使用 Clojure Var 做同样的事情
    • 阅读器宏#'xxx被翻译成(var xxx)
    • 阅读器宏@xxx被翻译成(deref xxx)

    所以我们(令人困惑!)有很多方法可以做同样的事情:

    (ns tst.demo.core
      (:use tupelo.core tupelo.test))
    
    (def wilma 3)
    ; `wilma` is a global symbol that points to
    ;     a Var object, that points to
    ;         a java.lang.Long object of value `3`
    
    (dotest
      (is= java.lang.Long (type wilma))
    
      (is= 3 (var-get (var wilma)))
      (is= 3 (var-get #'wilma))
    
      ; `deref` and `var-get` are interchangable
      (is= 3 (deref (var wilma)))
      (is= 3 (deref #'wilma))
    
      ; the reader macro `@xxx` is a shortcut that translates to `(deref xxx)`
      (is= 3 @(var wilma))
      (is= 3 @#'wilma)) ; Don't do this - it's an abuse of reader macros.
    

    【讨论】:

    • 这里有很多很好的信息 - 谢谢!但是,我发现关于var?var-getrun-jetty 的内容令人困惑。我在github.com/ring-clojure/ring/tree/master/ring-jetty-adapter 中找不到对var?var-get 的任何引用事实上,这似乎是由Clojure 运行时自动处理的,正如@OlegTheCat 在他的回答中所建议的那样,不需要明确的if 和调用var-get
    • 非常感谢您提供如此详细的解释!
    【解决方案2】:

    已经有几个很好的答案了。只是想添加这个警告:

    (defn f [] 10)
    (defn g [] (f))
    (g) ;;=> 10
    (defn f [] 11)
    
    ;; -Dclojure.compiler.direct-linking=true
    (g) ;;=> 10
    
    ;; -Dclojure.compiler.direct-linking=false
    (g) ;;=> 11
    

    因此,当direct linking 开启时,通过 var 进行的间接调用被直接静态调用替换。与处理程序的情况类似,但随后使用 每个 var 调用,除非您明确引用 var,例如:

    (defn g [] (#'f))
    

    【讨论】:

      【解决方案3】:

      希望这个小例子能让你走上正轨:

      > (defn your-handler [x] x)
      #'your-handler
      
      > (defn wrap-inc [f]
          (fn [x]
            (inc (f x))))
      > #'wrap-inc
      
      > (def your-app-with-var (wrap-inc #'your-handler))
      #'your-app-with-var
      
      > (def your-app-without-var (wrap-inc your-handler))
      #'your-app-without-var
      
      > (your-app-with-var 1)
      2
      
      > (your-app-without-var 1)
      2
      
      > (defn your-handler [x] 10)
      #'your-handler
      
      > (your-app-with-var 1)
      11
      
      > (your-app-without-var 1)
      2
      

      对此的直觉是,当您在创建处理程序时使用 var 时,您实际上是在传递一个带有某个值的“容器”,其内容可以通过定义具有相同名称的 var 来更改。当你不使用 var 时(比如在 your-app-without-var 中),你传递的是这个“容器”的当前值,它不能以任何方式重新定义。

      【讨论】:

        猜你喜欢
        • 2010-11-25
        • 2015-05-06
        • 1970-01-01
        • 2020-06-01
        • 1970-01-01
        • 2014-05-17
        • 1970-01-01
        • 2013-09-04
        相关资源
        最近更新 更多