我正在阅读电子书Functional Programming Patterns in Scala & Clojure并找到导致此问题的代码示例。
这段代码用于比较两个Person对象。比较算法是 - 首先比较他们的FNames,如果相等则比较他们的LName,如果相等则然后比较他们的MNames。
书中给出的Clojure代码(或多或少)
(def person1 {:fname "John" :mname "Q" :lname "Doe"})
(def person2 {:fname "Jane" :mname "P" :lname "Doe"})
(defn fname-compare [p1 p2]
(do
(println "Comparing fname")
(compare (:fname p1) (:fname p2))))
(defn lname-compare [p1 p2]
(do
(println "Comparing lname")
(compare (:lname p1) (:lname p2))))
(defn mname-compare [p1 p2]
(do
(println "Comparing mname")
(compare (:mname p1) (:mname p2))))
(defn make-composed-comparison [& comparisons]
(fn [p1 p2]
(let [results (for [comparison comparisons] (comparison p1 p2))
first-non-zero-result
(some (fn [result] (if (not (= 0 result)) result nil)) results)]
(if (nil? first-non-zero-result)
0
first-non-zero-result))))
(def people-comparision-1
(make-composed-comparison fname-compare lname-compare mname-compare))
(people-comparision-1 person1 person2)
;Output
;Comparing fname
;Comparing lname
;Comparing mname
;14
根据这个样本,它将进行所有三个比较,即使第一个返回非零。在这种情况下,它不是一个问题。但是,如果我编写了惯用的C#代码,那么该代码只会进行一次比较并退出。示例C#代码
public class Person {
public string FName {get; set;}
public string LName {get; set;}
public string MName {get; set;}
}
var comparators =
new List<Func<Person, Person, int>> {
(p1, p1) => {
Console.WriteLine("Comparing FName");
return string.Compare(p1.FName, p2.FName);
},
(p1, p1) => {
Console.WriteLine("Comparing LName");
return string.Compare(p1.LName, p2.LName);
},
(p1, p1) => {
Console.WriteLine("Comparing MName");
return string.Compare(p1.MName, p2.MName);
}
};
var p1 = new Person {FName = "John", MName = "Q", LName = "Doe"};
var p2 = new Person {FName = "Jane", MName = "P", LName = "Doe"};
var result =
comparators
.Select(x => x(p1, p2))
.Where(x => x != 0)
.FirstOrDefault();
Console.WriteLine(result);
// Output
// Comparing FName
// 1
将上述代码简单地翻译成clojure给了我
(defn compose-comparators [& comparators]
(fn [x y]
(let [result
(->> comparators
(map #(% x y))
(filter #(not (zero? %)))
first)]
(if (nil? result)
0
result))))
(def people-comparision-2
(compose-comparators fname-compare lname-compare mname-compare))
(people-comparision-2 person1 person2)
;Output
;Comparing fname
;Comparing lname
;Comparing mname
;14
这不是我的预期。我读某处,出于性能原因或某事,clojure一次处理一个序列的32个元素。什么是获得类似于C#代码的输出/行为的惯用Clojure方法?
以下是我的尝试。然而,它并不觉得“clojurey”。
(defn compose-comparators-2 [& comparators]
(fn [x y]
(loop [comparators comparators
result 0]
(if (not (zero? result))
result
(let [comparator (first comparators)]
(if (nil? comparator)
0
(recur (rest comparators) (comparator x y))))))))
(def people-comparision-3
(compose-comparators-2 fname-compare lname-compare mname-compare))
(people-comparision-3 person1 person2)
;Output
;Comparing fname
;14
修改:
根据这个问题的答案以及answer to a related question,我认为如果我需要提前退出,我应该明确这个问题。一种方法是将集合转换为惰性集合。另一种选择是使用reduced
从reduce循环中提前退出。
凭借我目前拥有的知识,我更倾向于选择明确的懒惰收集路线。使用以下函数是否存在问题 -
(defn lazy-coll [coll]
(lazy-seq
(when-let [s (seq coll)]
(cons (first s) (lazy-coll (rest s))))))
这样我就可以像往常一样使用map
,remove
。
答案 0 :(得分:1)
我用你的代码做了一些测试,它发生了:
((compose-comparators fname-compare lname-compare mname-compare) person1 person2)
是否按预期工作,仅比较fname
。
根据this blog post,Clojure中的懒惰可能不会像我们想象的那样严格执行:
作为一种语言的Clojure在默认情况下并不是懒惰的(不像 Haskell)因此懒惰可能会与严格的评估混在一起 导致令人惊讶和未经优化的后果。
答案 1 :(得分:1)
我认为你正在遇到分块序列。我不是这方面的专家,但我的理解是,根据你所拥有的序列的类型,clojure可以用32个元素的块来评估它,而不是完全懒惰。
e.g。你的第一个代码(没有按预期工作)是有效的:
;; I renamed your compare fns to c1, c2, c3
(->> [c1 c2 c3] ; vector; will be chunked
(map #(% person1 person2))
(filter #(not (zero? %)))
first)
comparing fname
comparing lname
comparing mname
14
与
(->> (list c1 c2 c3) ; list i.e. (cons c1 (cons c2 (cons c3 nil)))
(map #(% person1 person2))
(filter #(not (zero? %)))
first)
;comparing fname
;14
鉴于这种不幸行为,您可能想尝试另一种方法。怎么样:
(基于Amith George的评论的固定版本)
(some (fn [f]
(let [result (f person1 person2)]
(if (zero? result) false result)))
[c1 c2 c3])
;comparing fname
;14
答案 2 :(得分:1)
yield
。它被称为reduced
。
仅lazy-seq
避免计算结果是偶然的,没有严格的保证不评估未使用的结果。这对性能有益(通常一次计算lazy-seq
的结果块而不是一次计算结果的速度更快。)
我们在这里想要的不是结果的懒惰,而是如果找到特定的结果就会短路,这就是reduced
的设计目标。
(defn compare-by-key
[k]
(fn [p1 p2]
(println "Comparing" (name k))
(compare (k p1) (k p2))))
(def fname-compare (compare-by-key :fname))
(def lname-compare (compare-by-key :lname))
(def mname-compare (compare-by-key :mname))
(defn make-composed-comparison [& comparisons]
(fn [p1 p2]
(or (reduce (fn [_ comparison]
(let [compared (comparison p1 p2)]
(when-not (zero? compared)
(reduced compared))))
false
comparisons)
0)))
(def people-comparison-1
(make-composed-comparison fname-compare lname-compare mname-compare))
我还尝试在这里做一些更惯用的事情,希望你的原始代码仍然可识别。
user> (people-comparison-1 person1 person2)
Comparing fname
14
答案 3 :(得分:1)
正如你所怀疑的那样,正如其他答案所指出的那样,问题在于分块序列并不像它们那样懒惰。
如果我们查看您的compose-comparators
功能(略微简化)
(defn compose-comparators [& comparators]
(fn [x y]
(let [result (->> comparators
(map #(% x y))
(remove zero?)
first)]
(if (nil? result) 0 result))))
...在people-comparison-2
中运行所有三次比较的原因是map
处理了块中的分块序列,您可以看到here。
一个简单的解决方案是用map
替换已移除的块:
(defn lazy-map [f coll]
(lazy-seq
(when-let [s (seq coll)]
(cons (f (first s)) (lazy-map f (rest s))))))
顺便说一句,您可以抽象出比较器函数的构造。如果我们定义
(defn comparer [f]
(fn [x y]
(println "Comparing with " f)
(compare (f x) (f y))))
......我们可以用它来定义
(def people-comparision-2
(apply compose-comparators (map comparer [:fname :lname :mname])))