何时使用 Var 而不是函数?

Posted

技术标签:

【中文标题】何时使用 Var 而不是函数?【英文标题】:When to use a Var instead of a function? 【发布时间】: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))

【问题讨论】:

【参考方案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,它会保存一个指向函数对象的“本地指针”,而外界无法更改本地指针所指的内容。

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

http://clojure.org/reference/evaluation http://clojure.org/reference/vars

更新

正如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 中),你传递的是这个“容器”的当前值,它不能以任何方式重新定义。

【讨论】:

以上是关于何时使用 Var 而不是函数?的主要内容,如果未能解决你的问题,请参考以下文章

在 Oracle 中何时使用 vsize 函数而不是 length 函数的有用示例?

何时使用函数模板而不是通用 lambda?

AngularJS:何时使用服务而不是工厂

AngularJS:何时使用服务而不是工厂

何时使用 trunc() 而不是 int() 将浮点类型数转换为整数更好?

为啥不能在 ngFor 中使用 var 而不是 let