【问题标题】:Recursive/nested with "rest" parameters使用“rest”参数递归/嵌套
【发布时间】:2019-11-14 00:48:55
【问题描述】:

我经常想用所有键构建映射,但不希望键在其值为 nil 时显示。所以:

(building-map :a 1 :b nil :c 3 :d nil :e 5)

会产生 {:a 1 :c 3 :e 5}。 (显然我事先并不知道这些值,但为了演示的目的对其进行硬编码很方便。)我决定构建我自己的忽略 nils 的 assoc 类型函数,称为“asif”:

(defn asif [m & kvs]
  (if (empty? kvs)
  m
    (if (second kvs)
      (recur (assoc m (first kvs) (second kvs)) (drop 2 kvs))
      (recur m (drop 2 kvs)))))

(我从查看 assoc 开始,所以我知道这是他们所谓的“幼稚”实现。但现在用它运行。)现在,当然,因为我很贪婪,我希望它递归地工作:

(asif {} :a 1 :b 2 :c 3 :d nil :e {:sub1 1 :sub2 nil :a 1})

这应该返回 {:a 1 :b 2 :c 3 :e {:sub1 1 :a 1}}。但它当然不会,因为它只是将“e”上的地图视为单个值。

我目前的解决方案是这样做:

(asif {} :a 1 :b 2 :c 3 :d nil :e (asif {} :sub1 1 :sub2 nil :a 1))

这行得通,而且很好,但是这种“我已经用 & 收集了 parms”的情况现在想递归它们,但我不能,这一直让我感到烦恼。我知道我可以:

  • 传入整个地图而不是使用 & 开始。
  • 编写宏

但是有没有办法使用 & 来做到这一点? (我在其他方法中使用了“应用”。)或者当你用 & 收集参数时,骰子已经投下?

更新:澄清一下,这并不是真的要删除 nil——不是要使用剩余参数(因此是主题标题)。这只是我定期遇到的一个问题,并且(在这个特定的例子中)我今天在思考“我可以使用 [& rest] 来编写一个递归地忽略 nil 值的 assoc 模拟”时碰巧再次遇到它?

【问题讨论】:

  • 您是否考虑过一种方法,即使用常规函数构建地图,然后使用递归修剪具有 nil 值的地图条目的函数?
  • 是的,那是我的第一个要点“通过整个地图”。我所拥有的一切都很好。我更感兴趣的是 & 是否真的可以在这里投入使用。

标签: clojure


【解决方案1】:

我认为,问题在于您在顶级调用和递归调用中所做的事情之间存在不匹配。在顶级调用中,您有一个映射和多个键/值对,但在递归调用中,您只有一个映射,因此您不再是真正的associng,您正在从现有映射中删除 nil。

所以我想说,既然你在做两件不同的事情,你需要两个不同的功能。

这是一种从嵌套映射中递归删除 nil 的相当简单的方法:

(defn recursively-prune-nils [m]
  (into {} (map (fn [[k v :as pair]] 
                  [k
                   (if (map? v)
                     (recursively-prune-nils v)
                     v)])
                (filter second m))))

我们不能在这里使用recur,因为它不会在尾部位置,所以如果地图嵌套非常深,这最终会爆炸。

然后您可以在 asif 函数中使用它:

(defn asif [m & kvs]
        (if (empty? kvs)
          m
          (if (second kvs)
            (if (map? (second kvs))
              (recur (assoc m (first kvs) (recursively-prune-nils (second kvs))) (drop 2 kvs))
              (recur (assoc m (first kvs) (second kvs)) (drop 2 kvs)))
            (recur m (drop 2 kvs)))))

这似乎是你想要的:

user> (asif {} :a 1 :b 2 :c 3 :d nil :e {:sub1 1 :sub2 nil :a 1})
{:a 1, :b 2, :c 3, :e {:sub1 1, :a 1}}

【讨论】:

  • 好吧,如果 ...:e 不是地图,而是矢量呢?像 :e [:sub1 1 :sub2 nil :a 1]?我想我仍然卡住了,因为 &rest 将东西收集到一个向量中并且传递一个向量是不一样的。至少我不认为 apply 会在这里工作。
  • 我以前从来没有遇到过这种情况,但是recur 不能与apply 一起使用:stackoverflow.com/a/3670521/365064 你仍然可以递归调用asif。不过,您还有另一个问题,那就是在:e 上写(apply asif...),此时您没有到assoc 的映射。那时,您可能有不同的asif... 但这似乎有很多额外的复杂性。而且我认为从语义上讲,有了子图,你真的不再是associng,所以尝试用相同的函数处理这两种情况是没有意义的。
【解决方案2】:

我会这样做(写成单元测试)。基本上,除非您真的非常需要,否则不要重新发明轮子。只需改用postwalk

The code:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [clojure.walk :as walk]
    [schema.core :as s]
    [tupelo.schema :as tsk]
  ))

(def kv-data [:a 1 :b nil :c 3 :d nil :e {:x 1 :y nil :z 1}])

(s/defn map-remove-nil-vals :- tsk/Map
  [map-in :- tsk/Map]
  (reduce-kv
    (fn [accum k v]
      (if (nil? v)
        accum
        (assoc accum k v)))
    {}
    map-in))

(defn walk-drop-mapentry-nil-vals
  [arg]
  (walk/postwalk (fn [item]
                   (if (map? item)
                     (map-remove-nil-vals item)
                     item))
    arg))

(dotest
  (let [map-data (apply hash-map kv-data)
        result   (walk-drop-mapentry-nil-vals map-data)]
    (is= (spyx-pretty result)
      {:e {:x 1, :z 1}, :c 3, :a 1}
      )))

结果:

Testing _bootstrap

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core

result => {:e {:x 1, :z 1}, :c 3, :a 1}

Ran 2 tests containing 1 assertions.
0 failures, 0 errors.

Passed all tests

【讨论】:

  • 嘿,Alan,这是一个很好的例子,但问题不在于如何删除 nil,而在于是否可以按照所述递归方式使用 [& rest] 参数.我的意思是,答案可能只是“不”。
猜你喜欢
  • 1970-01-01
  • 2016-12-30
  • 1970-01-01
  • 2016-02-06
  • 2018-05-28
  • 2021-05-06
  • 1970-01-01
  • 2018-06-14
  • 1970-01-01
相关资源
最近更新 更多