clojure的C#等价于产量吗?

时间:2014-09-25 13:07:57

标签: clojure

我正在阅读电子书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))))))

这样我就可以像往常一样使用mapremove

4 个答案:

答案 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])))