如何在 clojure 中映射很少使用的状态?

Posted

技术标签:

【中文标题】如何在 clojure 中映射很少使用的状态?【英文标题】:How do I map with rarely used state in clojure? 【发布时间】:2017-11-06 08:55:33 【问题描述】:

情况如下:我正在转换一个值序列。每个值的转换分解为许多不同的情况。大多数值彼此完全独立。然而,有一种特殊情况需要我记录到目前为止我遇到过多少特殊情况。在命令式编程中,这非常简单:

int i = 0;
List<String> results = new ArrayList<>();
for (String value : values) 
  if (case1(value)) 
    results.add(handleCase1(value));
   else if (case2(value)) 
  ...
   else if (special(value)) 
    results.add(handleSpecial(value, i));
    i++;
  

但是在 Clojure 中,我想出的最好的方法是:

(first 
 (reduce 
  (fn [[results i] value]
      (cond
       (case-1? value) [(conj results (handle-case-1 value)) i]
       (case-2? value) ...
       (special? value) [(conj results (handle-special value i))
                         (inc i)]))
  [[] 0] values))

考虑到如果没有特殊情况,这会变得非常难看:

(map #(cond 
       (case-1? %) (handle-case-1 %)
       (case-2? %) ...)
      values)

问题是我在缩减过程中手动将一个序列拼接在一起。此外,大多数情况下甚至不关心索引,但仍必须将其传递给下一个归约步骤。

这个问题有更清洁的解决方案吗?

【问题讨论】:

【参考方案1】:

您可以只使用一个原子来跟踪它:

(def special-values-handled (atom 0))

(defn handle-cases [value]
  (cond
    (case-1? value) (handle-case-1 value)
    (case-2? value) ...
    (special? value) (do (swap! special-values-handled inc)
                         (handle-special @special-values-handled value))))

那你就可以了

(map handle-cases values)

【讨论】:

我忘了,handle-special 也需要知道 Clojure 变体中的索引。我已经相应地编辑了帖子。所以我绝对不只是计算特殊情况。 但是 map-indexed 增加了 every 值的索引。我只想在遇到特殊值时增加它。 我认为只有一个原子就是要走的路。已更新。【参考方案2】:

正如 Alejandro 所说,atom 允许人们轻松跟踪可变状态并在需要时使用它:

(def special-values-handled (atom 0))

(defn handle-case-1 [value]  ...)
(defn handle-case-2 [value]  ...)
...
(defn handle-special [value]
  (let [curr-cnt (swap! special-values-handled inc)]
    ...<use curr-cnt>... )
  ...)

(defn handle-cases [value]
  (cond
    (case-1? value)   (handle-case-1  value)
    (case-2? value)   (handle-case-2  value)
    ...
    (special? value)  (handle-special value)
    :else (throw (IllegalArgumentException. "msg"))))

...
(mapv handle-cases values)

当一个可变状态是解决问题的最简单方法时,不要害怕使用原子。


我有时使用的另一种技术是使用“上下文”映射作为累加器:

(defn handle-case-1 [ctx value] (update ctx :cum-result conj (f1 value)))
(defn handle-case-2 [ctx value] (update ctx :cum-result conj (f2 value)))
(defn handle-special [ctx value]
  (-> ctx
    (update :cum-result conj (f-special value))
    (update :cnt-special inc)))

(def values ...)
(def result-ctx
  (reduce
    (fn [ctx value]
      (cond
        (case-1? value) (handle-case-1 value)
        (case-2? value) (handle-case-2 value)
        (special? value) (handle-special value i)))
    :cum-result  []
     :cnt-special 0
    values))

【讨论】:

地图索引在这里似乎是多余的。特殊值处理原子已经在处理特殊值的索引。 也许我误解了您关于需要索引的其他评论。在这种情况下,请坚持使用mapmapv。更新以显示计数器对 handle-special 的使用【参考方案3】:

有时使用looprecur 的代码看起来比使用reduce 的等效代码更好。

(loop [[v & more :as vs] values, i 0, res []]
  (if-not (seq vs)
    res
    (cond
      (case-1? v) (recur more i (conj res (handle-case-1 v)))
      (case-2? v) (recur more i (conj res (handle-case-2 v)))
      (special? v) (recur more (inc i) (conj res (handle-special i v))))))

由于似乎有一些需求,这里有一个产生惰性序列的版本。关于过早优化和保持简单的习惯性警告适用。

(let [handle (fn handle [[v & more :as vs] i]
               (when (seq vs)
                 (let [[ii res] (cond
                                 (case-1? v) [i (handle-case-1 v)]
                                 (case-2? v) [i (handle-case-2 v)]
                                 (special-case? v) [(inc i) (handle-special i v)])]
                   (cons res (lazy-seq (handle more ii))))))]
  (lazy-seq (handle values 0)))

【讨论】:

很好,但是手动使用惰性序列的递归函数更好(因为惰性比被困在产生向量要好)。 我非常喜欢这个解决方案。尽管我的良心仍然迫使我使 res 成为短暂的。另外,@amalloy 你有什么想法? @SebastianOberhoff 正是编辑中添加的内容。虽然我会说这根本不是优化,更不用说过早了:它恰恰相反,放弃了少量吞吐量,让调用者可以灵活地生成这个函数的输入和使用它的输出。【参考方案4】:

您想要一种纯粹的函数式方法吗?尝试使用 Map 集合来满足您的临时价值需求。这样可以使您的结果保持整洁,并且可以在需要时轻松访问这些临时值。

当我们遇到特殊值时,我们还会更新地图中的计数器以及结果列表。这样我们可以在处理过程中使用reduce 来存储一些状态,但在没有atoms 的情况下保持一切纯粹的功能。

(def transformed-values
  (reduce
    (fn [:keys [special-values-count] :as m value]
      (cond
        (case-1 value) (update m :results conj (handle-case-1 value))
        (case-2 value) (update m :results conj (handle-case-2 value))
        ...
        (special-case? value) (-> m
                                  (update :results conj (handle-special value special-values-count))
                                  (update :special-values-count inc))
        :else m))
    :results [] :special-values-count 0
    your-list-of-string-values))

(:results transformed-values)
;=> ["value1" "Value2" "VALUE3" ...]

(:special-values-count transformed-values)
;=> 2

【讨论】:

【参考方案5】:

为此使用 volatile! 没有任何问题 - 在您的情况下,它不会逃避表达式的上下文,也不会产生任何可变性或线程复杂性:

(let [i (volatile! 0)]
  (map #(cond 
          (case-1? %) (handle-case-1 %)
          (case-2? %) (handle-case-2 %)
          (special? %) (do (handle-special % @i)
                           (vswap! i inc)))
       values)

如果您使用 Clojure atom。

【讨论】:

以上是关于如何在 clojure 中映射很少使用的状态?的主要内容,如果未能解决你的问题,请参考以下文章

如何Clojure.Spec引用类型(如原子)?

Clojure:如何将映射条目的惰性序列转换为结构映射?

如何在 Clojure/Compojure/Ring 中将映射转换为 URL 查询字符串?

如何从 Clojure 调用 C++ 程序以使程序保持打开状态?

Clojure 中纸牌游戏的状态

如何具体化 Prolog 的回溯状态以执行与 Clojure 中的“lazy seq”相同的任务?