何时使用 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
这样的特殊形式是像 if
或 def
这样的内置编译器,并且不与常规函数相同。 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-3
、add-3-fn
和add-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-get
和 deref
都使用 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-get
和run-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 函数的有用示例?