关于匿名“自引用”数据结构的建议/讨论

时间:2012-07-25 13:57:58

标签: macros clojure self-reference

对于任何错误的术语表示歉意 - 我对计算机科学很陌生,而且我几乎只知道Clojure(但我想我会说我很清楚)。

所以,我还没有对此做过大量研究,但我有时发现在编写Clojure代码时能够引用某些数据结构的中间版本很有用。我在"来自该数据结构(很像let)。快速举例:

=> (self-ish {:a 10
              :b (inc (this :a))
              :c (count (vals this))})
=> {:a 10, :b 11, :c 3}
=> (self-ish ["a" "b" (reduce str this)])
=> ["a" "b" "ab"]
//Works in any nested bits too
=> (self-ish [1 2 3 [4 5 (first this)] 6 [7 [8 (cons (second this) (nth this 3))]]])
=> [1 2 3 [4 5 1] 6 [7 [8 (2 4 5 1)]]]

这个想法是结构逐步建立起来,并且在任何阶段都能够将当前的中间结构称为this。这是我当前实现的代码:

//Random straightforward but helpful definitions
(defn map-entry? [obj]
  (instance? clojure.lang.AMapEntry obj))
(def Map clojure.lang.IPersistentMap)
(def Vector clojure.lang.IPersistentVector)
(def List clojure.lang.IPersistentList)
(def Set clojure.lang.IPersistentSet)

(defn append
  [x coll]
  (if-not coll x
    (condp instance? coll
      Map (if (empty? x) coll
            (assoc coll (first x) (second x)))
      Vector (conj coll x)
      Set (conj coll x)
      List (apply list (concat coll [x]))
      (concat coll [x]))))

(defn build-this
  [acc-stack acc]
  (->> (cons acc acc-stack)
       (drop-while list?)
       (drop-while (every-pred empty? identity))
       (reduce append)))

(defn self-indulge
  [acc-stack acc form]
  ;//Un-comment the following to see it print intermediate stages of processing
  #_(println "this:" (build-this acc-stack acc) "\n  at:" form)
  (append (cond
            (coll? form) (reduce (partial self-indulge (cons acc acc-stack))
                                 (if (map-entry? form) []
                                   (empty form))
                                 form)
            (= (quote this) form) (build-this acc-stack acc)
            :else form)
          acc))

(defmacro self-ish
  [form]
  (self-indulge () nil form))

append函数将一个项追加到一个集合上,并返回相同类型的集合。 self-indulge函数有一个标准的类似reduce的累加器,它只构建表单元素。它还有一个累加器堆栈,每次self-indulge自身重复时它会变长。关键在于跟踪其他更高的"更高的"累加器,以便this将是整个结构,而不仅仅是本地部分。 self-ish宏很好地包裹self-indulge(使用partial调用自身,因此无法穿上宏裤)。

编辑:示例用例 对我来说,这个宏是关于尝试提高代码可读性,而不是真正扩展功能。我发现这有用的地方是我手写的结构有部分冗余的数据 - 或者可能依赖"是一个更好的词。可以更容易地阅读代码并查看数据结构的不同部分,如果我在结构的一个部分中修改数据值并希望将更改反映在其他部分中,它也会很有用。例如:

=> (self-ish {:favorite-books (list "Crime and Punishment" "Mrs. Dalloway")
              :favorite-things (list* "Ice Cream" "Hammocks" (this :favorite-books)})
=> {:favorite-things ("Ice Cream" "Hammocks" "Crime and Punishment" "Mrs. Dalloway"),
    :favorite-books ("Crime and Punishment" "Mrs. Dalloway")}

在人们可能真的希望将某些内容包含在数据中,而不是使用某些函数动态派生时,它也可能很有用。这些情况可能非常罕见,我认为当你可以使用干净的函数操作它时,不必要地纠缠数据是个坏主意。

我的主要问题:

  1. 这实际上是否有用,或者引起的歧义/复杂性是否过多?我想我并不是唯一想要/使用这种类型的宏。什么是其他人'在这里体验?你用这样的东西吗?你找到了更好的解决方法吗?在任何Clojure库中都有这样的原因吗?或者有什么东西我还没见过?
  2. 我可能会使用更好的命名约定 - 而不是self-ishthis?例如,也许this太满了OOP意思,我不确定,我基本上只熟悉Clojure。
  3. 我是计算机科学的新手,是否存在与此类事物相关的可访问和信息资源 - 我想我会称之为匿名自我引用(也许是反身是一个更好的词?)数据结构?我还没有发现任何既平易近人又信息丰富的内容。
  4. 有没有更好的方法来编写self-ish宏?上面,我已经包含了我目前的版本,但我不能感觉可能有一种更简单的方式。
  5. 关于什么是最明智的"我有各种各样的问题。实施细节。

    • Traversal:首先是广度还是先深度?如果深度优先,预订,后序或订购?现在,我相信它的深度优先预售,这对我来说很有意义,但也许它有一些我没有注意到的缺点。
    • 订购问题 :( See here for a related previous question of mine)在{}内(即手工编写的地图),无法正确维护订单(8位以上)条目)没有使用array-mapsorted-map - 换句话说,超过8个地图条目,{}使用是不安全的。也许不是手写的顺序,宏可以做一些奇特的魔术来处理一些"理想的"订购?或者最好将所有地图包裹在(array-map ...)而不是令人赏心悦目的{}中?

      //Showing maps with 9 entries failing
      => (self-ish {:a 1
                    :b (inc (this :a))
                    :c (inc (this :b))
                    :d (inc (this :c))
                    :e (inc (this :d))
                    :f (inc (this :e))
                    :g (inc (this :f))
                    :h (inc (this :g))
                    :i (inc (this :h))})
      => NullPointerException   clojure.lang.Numbers.ops (Numbers.java:942)
      //8 works fine
      => (self-ish {:a 1
                    :b (inc (this :a))
                    :c (inc (this :b))
                    :d (inc (this :c))
                    :e (inc (this :d))
                    :f (inc (this :e))
                    :g (inc (this :f))
                    :h (inc (this :g))})
      => {:h 8, :g 7, :f 6, :e 5, :d 4, :c 3, :b 2, :a 1}
      
    • 串行:正如我已经写过的那样,宏通过串行处理其元素来避免无限递归,类似于let,但这确实会产生奇怪的行为。例如,在上面的简单示例中,(reduce str this)返回"ab",因为this在该步骤中为["a" "b"]。也许有时创建某种无限懒惰序列会有用吗?如果是的话,该如何实施呢?

    • 地图条目:现在,地图条目被视为向量,但由于在任何中间步骤可以调用this的方式,因此完全有可能获得{{1来自具有"尚未"的键的值被分配了一个值。这就是为什么在我的第一个快速示例中,nil最终映射到3 - 因为中间有一个:c对应nil,并且也被计算在内。你认为这有必要修理吗?
    • 非宏实用程序:在宏上下文之外仅使用:c是微不足道的,但这可能有用吗?
  6. 感谢阅读,感谢任何帮助:)

2 个答案:

答案 0 :(得分:2)

在方案中,这是使用(letrec ...)完成的,它允许您引用结构本身内部的数据结构的名称。因此,如果您想要定义自己的方法,那么实现它可能更有意义。你可以使用Is it possible to create circular references in Clojure?

的答案中描述的参考技巧来做到这一点

letrec的一个优点是它具有用户指定的名称(如果您的宏嵌套,则this被遮蔽)。

[编辑删除对类型的评论,因为我不理解您的宏。]

[更新]此外,您可能对clojure第8.5.1节中的回指的讨论感兴趣

答案 1 :(得分:2)

这种方法对我来说“感觉”有点不对,虽然我不太清楚为什么。也许我不喜欢地图构造依赖于秩序的想法....

虽然说这是一个非常容易编写的宏,但你实际上想要扩展的东西:

(let [this {}
      this (assoc this :a 1)
      this (assoc this :b (+ (this :a) 3))]
  this)

因此,适当的宏将是(对于地图案例):

(defmacro self-ish [bindings]
  `(let [~'this {}
         ~@(mapcat 
           #(do `(~'this (assoc ~'this ~@%)) )    
           (partition 2 bindings) )]
    ~'this))

(self-ish [:a 1
           :b (+ (this :a) 3)])
=> {:b 4, :a 1}

请注意,由于地图绑定表单是无序的,因此我将绑定形式设为矢量。

仍然不确定我有多喜欢这个成语。我首选的方法是定义一个带有let形式的结构,并为临时计算提供有意义的名称,例如:

(let [meaningful-foo (something)
      meaningful-bar (something-else)]
   {:foo meaningful-foo
    :bar meaningful-bar
    :baz (some-calculation meaningful-foo meaningful-bar)})