何时使用 Var 而不是函数?

When to use a Var instead of a function?

我正在阅读这本书 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}))

是的,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 ...) 形式引入了 不可见的 第三个元素,即 Var。所以如果我们说

(def wilma 3)

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

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

我们可以像这样“短路”Var 的自动求值:

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

> #'wilma
#'clj.core/wilma

其中 reader 宏 #'(引号)是调用 (var ...) 特殊形式的 shorthand 方式。请记住,像 var 这样的特殊形式是像 ifdef 这样的内置编译器,并且是 而不是 与常规函数相同。 var 特殊形式 return 是附加到符号 wilma 的 Var 对象。 clojure REPL 使用相同的 shorthand 打印 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 将函数对象表示为字符串的方式。

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

如果您键入如下内容:

(jetty/run-jetty handler)

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

(jetty/run-jetty (var handler))

然后指向处理函数对象的Var将被传递给run-jetty。然后,run-jetty 将不得不使用 if 语句或等效语句来确定它收到了什么,如果收到 Var 而不是函数,则调用 (var-get ...) 。因此,每次通过 (var-get ...) 都会 return 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 将 SILENTLY 将其解引用到函数对象中,然后调用该函数对象与提供的参数。所以我们看到 add-3add-3-fnadd-3-var 这三个都可以工作。这就是 Jetty 中正在发生的事情。它永远不会意识到您给了它一个 Var 对象而不是一个函数,但是 Clojure 在不告诉您的情况下神奇地修补了这种不匹配。

Sidebar: Please note this only works since our "jetty" is actually the Clojure wrapper code ring.adapter.jetty, and not the actual Java webserver Jetty. If you tried to depend on this trick with an actual Java function instead of a Clojure wrapper, it would fail. Indeed, you must use a Clojure wrapper like proxy in order to pass a Clojure function to Java code.

如果您将 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
  • reader宏#'xxx翻译成(var xxx)
  • reader宏@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.

另注

(def ...) 特殊形式 return 是它创建的 clojure.lang.Var 对象。通常,(def ...) 形式仅在 Clojure 源文件(或 REPL)的顶层使用,因此 return 值被静默丢弃。但是,也可以捕获对创建的 Var 对象的引用:

(let [p (def five 5)
      q (var five)] 
  (is= clojure.lang.Var
       (type p)
       (type q))

  (is= 6
       (inc five)
       (inc (var-get p))
       (inc (deref q)))

  (is (identical? p q)))

这里我们创建了一个指向数字5的全局变量fivedef 形式的 return 值在本地值 p 中被捕获。我们使用 (var ...) 特殊形式来获得指向同一个 Var 对象的引用 q

第一个测试表明pq都是clojure.lang.Var类型。中间测试显示了三种访问值 5 的方法。正如预期的那样,所有检索值 5 递增以产生 6。最后一个测试验证 pq 都指向同一个 Java 对象(即只有一个 clojure.lang.Var 对象指向整数 5) .

一个 Var 甚至可以指向另一个 Var 而不是一个数据值:

(def p (def five 5))   ; please don't ever do this

虽然它有效,但我想不出这样做的正当理由。

希望这个小例子能让您步入正轨:

> (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 时,您实际上是在传递具有某个值的 "container",其内容可以在将来通过定义具有相同名称的 var 来更改。当您不使用 var 时(如在 your-app-without-var 中),您传递的是此 "container" 的当前值,它不能以任何方式重新定义。

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

(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 的间接调用将替换为直接静态调用。与处理程序的情况类似,但是随后 every var 调用,除非您明确引用 var,例如:

(defn g [] (#'f))