【问题标题】:Using "maybe ARefs" in higher-order functions在高阶函数中使用“maybe ARefs”
【发布时间】:2014-04-12 15:27:04
【问题描述】:

我想知道如何包装一个函数(或函数定义),使它看起来似乎不知道传递给它的参数是可变的还是不可变的——但如果给定的任何参数是可变的,它应该取消引用每次调用时的参数,获取当前值。

我可以编写一个函数,该函数要求每个参数都是可变存储,然后在每次调用时取消引用。但是在 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),或者基于在观察算法的持续性能时确定的其他一些标准。我的算法是并行的,每个线程都需要看到变化。在我更改参数时重新启动算法会破坏目的。

【问题讨论】:

    标签: clojure


    【解决方案1】:

    看来我在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))
    

    然后,我们在定义 baz1baz2 时采用 eval 命中,而在随后使用它们时。为定义baz1baz2 生成的代码,以及它们在使用时的性能,与我们所做的完全一样

    (def baz1 (fn [x] (+ 40 2 x)))
    (def baz2 (fn [x] (+ @my-ref @my-atom x)))
    

    说了这么多……

    如果适合您的用例,原始的“无评估”解决方案是我更喜欢的:

    (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 的答案之间的主要区别在于测试/分支已移到返回的函数之外,该函数现在关闭了结果。

    【讨论】:

      【解决方案2】:

      使用评估

      要获得足够聪明的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))
      

      据我所知,fg 具有相同的性能特征。

      没有评估

      这似乎相当有效地工作:

      (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"
      

      【讨论】:

      • 应您的要求,我发布了我对您相关问题的回答中的代码。我实际上更喜欢您在这里的原始答案(正如我在此处的回答中所评论的那样)。不过,我并不完全了解您的用例;有什么理由不充分吗?
      • 有两点我不喜欢它:首先,我必须写 (* (a) (b) x) 而不是 (* a b x)。那是相当轻微的。其次,这主要是事物的原理——我喜欢函数式语言的原因之一是我可以编写代码来为我编写代码。我的生产力提高了,我的错误减少了。但似乎我遇到了 Clojure 功能的限制,我很沮丧。 :-) 我花了一整天的时间——我无法证明这一点。但我一路上学到了很多东西!
      • 好吧,我想还有一件事。当我思考这个问题并意识到我想要的——根据某些规则修改的函数——我找到了一个解决方案,它会给我非常相似的东西。但不完全是这样。我不喜欢满足于“变通办法”和“近似值”。我喜欢你的代码,因为它给了我手写的功能。
      • 酷。返回一条评论,为了语义和任何未来的读者(我在重复我几乎可以肯定你已经知道的内容),你最初的解决方案是“功能性”解决方案 - 函数、闭包、一流和更高级别的组合-订单函数。我提供的那个使用了 lisps 的“同质性”和元编程能力,这不是所有函数式语言的特征。
      • 我先写了 Lisp,然后不假思索地把它改成了“函数式”。但你是对的。我刚刚与一位 Haskell 程序员进行了一次对话,他认为同音性对于一门语言来说是不必要的属性。
      猜你喜欢
      • 1970-01-01
      • 2023-04-03
      • 1970-01-01
      • 2016-05-07
      • 2021-05-25
      相关资源
      最近更新 更多