我想知道如何包装一个函数(或函数定义),使得它变得看似不可知传递给它的参数是可变的还是不可变的 - 但如果给出的任何参数是可变的,它应该取消引用每次调用时参数,以获得当前值。
我可以编写一个函数,要求每个参数都是可变存储,然后每次调用时都会解引用。但是,在Clojure中取消引用可变存储的性能受到了影响(非常非常小,我知道!)。在我的具体使用案例中,这些实际上是瓶颈操作,小到足以解除引用以产生影响,并且重复数十万到数百万次(更多关于我的用例如下,但是现在让我们假设这很重要)。所以我不想在我不需要它的情况下使用可变数据。如果从外部看,代码似乎不关心初始参数是可变的还是不可变的,那将是很好的。 让我们说,为简单起见,该功能如下:
(defn foo [a b]
(fn [x] (* a b x)))
(def instance (foo 3 4))
(instance 5) ; <- 60
(instance 8) ; <- 96
我想要一个足够聪明的foo
:
(def a (agent 3))
(def b (agent 4))
(foo a b) ; <- (fn [x] (* (deref a) (deref b) x))
(foo a 4) ; <- (fn [x] (* (deref a) 4 x))
(foo 3 4) ; <- (fn [x] (* 3 4 x))
但是,我第一次尝试做某事时使用引用和取消引用(自然,对吧?它是什么宏使用的!),它给了我一个关于在代码中嵌入对象的一个讨厌的错误(一个非常类似的问题,不同用例,是discussed here)。我的下一次尝试给了我一个奇怪的(和大量的)slowdown in runtimes。
有没有人知道这样做的好方法?
背景
我正在研究一些机器学习算法。在典型情况下,用户将使用一组特定参数初始化算法,然后在一组数据上运行它。但有时用户/用户定义的代码可能希望在算法运行时修改参数,或者基于时间(例如,simulated annealing),或者基于在观察算法时确定的一些其他标准。持续表现。我的算法是并行化的,每个线程都需要查看更改。当我更改参数时重新启动算法将无法达到目的。
答案 0 :(得分:2)
使用Eval
为了让foo
聪明到可以做你想做的事,你可以使用run-time expression modification:
(defn maybe-deref-expr
[vals params body]
(let [smap (zipmap params
(map (fn [val sym]
(if (instance? clojure.lang.IDeref val)
(list 'deref sym)
sym))
vals
params))
body* (clojure.walk/postwalk-replace smap body)
gen (eval (list 'fn params body*))]
(apply gen vals)))
(defmacro defn-ref-agnostic
[name params body]
`(defn ~name
~params
(maybe-deref-expr ~params '~params '~body)))
(defn-ref-agnostic foo
[a b]
(fn [x] (* a b x)))
(defn foo-baseline
[a b]
(fn [x] (* a b x)))
(def f (foo 3 4))
(def g (foo 3 4))
据我所知,我的机器f
和g
具有相同的性能特征。
没有评估
这似乎有效地 :
(defn aref? [x] (instance? clojure.lang.ARef x))
(defn foo-wraps [& args]
(map (fn [i] (if (aref? i)
#(deref i)
#(identity i)))
args))
(defn foo [a b]
(let [[a b] (foo-wraps a b)]
(fn [x] (* (a) (b) x))))
我想这可能是HotSpot拯救的一个例子?如果我没有通过任何ARefs,那么只有少数几次运行后表现非常接近原始配方:
(def a (ref 3))
(def b (ref 4))
(def f (foo 3 4))
(def g (foo a b))
(defn h [x] (* 3 4 x))
user=> (time (dotimes [n 10000] (f n)))
"Elapsed time: 7.38648 msecs"
"Elapsed time: 3.45071 msecs"
"Elapsed time: 3.087424 msecs"
"Elapsed time: 2.836596 msecs"
user=> (time (dotimes [n 10000] (g n)))
"Elapsed time: 13.076024 msecs"
"Elapsed time: 4.235882 msecs"
"Elapsed time: 4.517663 msecs"
"Elapsed time: 3.940946 msecs"
user=> (time (dotimes [n 10000] (h n)))
"Elapsed time: 4.056389 msecs"
"Elapsed time: 2.499129 msecs"
"Elapsed time: 3.064487 msecs"
"Elapsed time: 2.631167 msecs"
答案 1 :(得分:2)
我似乎在related question回答了这个问题,其中有maybe-deref-expr
示例。这段代码在Timothy Dean's own answer中重复了,还有他为它写的一些不错的宏糖,所以一定要查看他的答案。这是maybe-deref-expr
的略微修改版本,可能更容易阅读。
(defn maybe-deref-expr
[values params body]
(let [valmap (zipmap params values)
deref? #(instance? clojure.lang.IDeref %)
body* (clojure.walk/postwalk
#(if (deref? (valmap %)) `(deref ~%) %)
body)
gen (eval `(fn ~params ~body*))]
(apply gen values)))
与Timothy Dean的宏观糖一起
(defmacro defn-ref-agnostic
[name params body]
`(defn ~name
~params
(maybe-deref-expr ~params '~params '~body)))
如果我们这样做
(defn-ref-agnostic add
[a b]
(+ a b))
然后我们得到一个缓慢的(eval
点击)add
,在需要时自动取消引用
(add 40 2) ;=> 42
(add (ref 40) (atom 2)) ;=> 42
但是,用例不是定义函数本身,而是关闭其他参数的函数生成器。
(defn-ref-agnostic add-to
[a b]
(fn [x] (+ a b x)))
现在,如果我们这样做
(def baz1 (add-to 40 2))
(def my-ref (ref 40))
(def my-atom (atom 2))
(def baz2 (add-to my-ref my-atom))
然后,我们会在eval
和baz1
定义时baz2
点击,而不后再使用。为baz1
和baz2
的定义生成的代码,以及使用它们时的性能,就像我们已经完成的那样
(def baz1 (fn [x] (+ 40 2 x)))
(def baz2 (fn [x] (+ @my-ref @my-atom x)))
有人说过......
原作&#34;没有评价&#34;解决方案,如果它适合您的用例,是我更喜欢的:
(defn foo [a b]
(let [[fa fb] (map #(if (instance? clojure.lang.IDeref %)
deref
identity)
[a b])]
(fn [x] (+ (fa a) (fb b) x))))
这仅在最多两个额外身份函数调用的低成本时引入了额外的间接级别。它比上面简单得多,而且非常灵活。这与other related question的答案之间的主要区别在于测试/分支已移出返回的函数,现在关闭结果。