8

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

所以,我还没有对此进行大量研究,但我有时发现它在编写 Clojure 代码时很有用,以便能够从该数据结构中引用一些“我所在的任何数据结构的中间版本” (很像 a 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-ishand this?比如可能this是太加载了OOP的意思,我不确定,我基本上只熟悉Clojure。
  3. 我对计算机科学很陌生,是否有与此类事物相关的可访问且信息丰富的资源——我想我会称之为匿名自我引用(也许反身是一个更好的词?)数据结构?我还没有发现任何既平易近人又信息丰富的东西。
  4. 有没有更好的方法来编写self-ish宏?上面,我已经包含了我当前的版本,但我无法摆脱可能有更简单方法的感觉。
  5. 我对什么可能是“最明智的”实施细节有各种疑问。

    • 遍历:是广度优先还是深度优先?如果深度优先,是前序、后序还是有序?现在,我相信它是深度优先预购,这对我来说很有意义,但也许它有一些我没有注意到的缺点。
    • 订单问题:(有关我之前的相关问题,请参见此处)在内部{}(即手写地图)中,如果不使用array-mapsorted-map- 换句话说,在 8 个地图条目以上,{}用法是不可能正确维护订单(超过 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}
      
    • Serial:正如我所写,宏通过串行处理其元素来避免无限递归,类似于let,但这确实会产生潜在的奇怪行为。例如,在我上面的快速示例中,(reduce str this)返回"ab"因为this["a" "b"]该步骤。也许有时创建某种无限的惰性序列会很有用?如果是这样,那将如何实施?

    • 映射条目:现在,映射条目被视为向量,但由于如何this在任何中间步骤调用,完全有可能nil从“尚未”分配值的键中获取值。这就是为什么在我的第一个快速示例中,:c最终映射到 3——因为中间有一个nil对应于:c,并且也被计算在内。你认为这值得修复吗?
    • 非宏实用程序:仅self-indulge在宏上下文之外使用是微不足道的,但这可能有用吗?

感谢阅读,任何帮助表示赞赏:)

4

2 回答 2

2

在方案中,这样做(letrec ...)可以让您引用结构本身内部的数据结构的名称。因此,如果您想定义自己的执行方式,那么实现它可能更有意义。您可以使用在是否可以在 Clojure 中创建循环引用的答案中描述的引用技巧来做到这一点?

letrec 的一个优点是它有一个用户指定的名称(如果您的宏是嵌套的,那么this它会被隐藏)。

[编辑删除类型的评论,因为我不明白你的宏。]

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

于 2012-07-25T23:47:58.000 回答
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)})
于 2012-07-26T03:34:49.347 回答