如何在 clojure 中映射很少使用的状态?
How do I map with rarely used state in clojure?
情况如下:我正在转换一个值序列。每个值的转换分解成许多不同的情况。大多数值彼此完全独立。然而,有一种特殊情况需要我记录到目前为止我遇到了多少特殊情况。在命令式编程中,这非常简单:
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)
问题是我在缩小过程中手动将序列拼接在一起。此外,大多数情况下甚至不关心索引,但仍必须将其传递给下一个缩减步骤。
这个问题有更简洁的解决方案吗?
你可以只用一个原子来跟踪它:
(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)
正如 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)
当可变状态是解决问题的最简单方法时,永远不要害怕使用原子。
我有时使用的另一种技术是使用 "context" 映射作为累加器:
(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))
有时使用 loop
和 recur
的代码看起来比使用 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))))))
由于好像有需求,这里提供一个产生lazy sequence的版本。关于过早优化和保持简单的习惯性警告适用。
(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)))
您想要纯函数式方法吗?尝试使用 Map 集合来满足您的临时价值需求。这可以使您的结果保持美观和干净,并且可以在需要时轻松访问这些临时值。
当我们遇到特殊值时,我们还会更新地图中的计数器以及结果列表。这样我们就可以在处理过程中使用 reduce
来存储一些状态,但在没有 atom
s 的情况下保持一切纯粹的功能。
(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
为此使用 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)
如果您使用的是低于 1.7 的 Clojure 或想以多线程方式(例如使用 pmap)执行此操作,则可以使用 atom
。
情况如下:我正在转换一个值序列。每个值的转换分解成许多不同的情况。大多数值彼此完全独立。然而,有一种特殊情况需要我记录到目前为止我遇到了多少特殊情况。在命令式编程中,这非常简单:
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)
问题是我在缩小过程中手动将序列拼接在一起。此外,大多数情况下甚至不关心索引,但仍必须将其传递给下一个缩减步骤。
这个问题有更简洁的解决方案吗?
你可以只用一个原子来跟踪它:
(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)
正如 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)
当可变状态是解决问题的最简单方法时,永远不要害怕使用原子。
我有时使用的另一种技术是使用 "context" 映射作为累加器:
(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))
有时使用 loop
和 recur
的代码看起来比使用 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))))))
由于好像有需求,这里提供一个产生lazy sequence的版本。关于过早优化和保持简单的习惯性警告适用。
(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)))
您想要纯函数式方法吗?尝试使用 Map 集合来满足您的临时价值需求。这可以使您的结果保持美观和干净,并且可以在需要时轻松访问这些临时值。
当我们遇到特殊值时,我们还会更新地图中的计数器以及结果列表。这样我们就可以在处理过程中使用 reduce
来存储一些状态,但在没有 atom
s 的情况下保持一切纯粹的功能。
(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
为此使用 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)
如果您使用的是低于 1.7 的 Clojure 或想以多线程方式(例如使用 pmap)执行此操作,则可以使用 atom
。