Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does clojure have the C# equivalent of yield?

Tags:

clojure

I was reading the ebook Functional Programming Patterns in Scala & Clojure and found a code sample that led to this question.

This piece of code is meant to compare two Person objects. The comparision algo is - First compare their FNames, if equal then compare their LName, if equal then compare their MNames.

Clojure code as given in the book (more or less)

(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

Thing is, as per this sample it will do all three comparisons even if the first one returned non zero. In this case its not an issue. However if I had written idiomatic C# code, then that code would have done only one comparison and exited. Sample C# code

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

A naive translation of the above code into clojure gives me

(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

And that is not what I expected. I read somewhere that clojure processes 32 elements of a sequence at a time for performance reasons or something. What is the idiomatic Clojure way to get the output/behaviour similar to the C# code?

The following is my attempt. However it doesn't feel "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

Edit :

Based on answers to this question as well as the answer to a related question, I think if I need early exit, I should be explicit about it. One way would be to convert the collection to a lazy one. The other option is to use reduced to exit early from the reduce loop.

With the knowledge I currently have, I am more inclined to opt for the explicit lazy collection route. Is there an issue with using the following function to do so -

(defn lazy-coll [coll]
  (lazy-seq 
    (when-let [s (seq coll)]
      (cons (first s) (lazy-coll (rest s))))))

This way I can use map, remove the way I normally would have.

like image 634
Amith George Avatar asked Sep 29 '22 19:09

Amith George


2 Answers

As you suspected yourself and as the other answers have noted, the problem is that chunked sequences are not as lazy as they might be.

If we look at your compose-comparators function (slightly simplified)

(defn compose-comparators [& comparators]
  (fn [x y]
    (let [result (->> comparators
                      (map #(% x y))
                      (remove zero?)
                      first)]
      (if (nil? result) 0 result))))

... the reason that all three comparisons are run in people-comparison-2 is that map deals with a chunked sequence in chunks, as you can see here.

A simple solution is to substitute a map with the chunkiness removed:

(defn lazy-map [f coll]
  (lazy-seq
    (when-let [s (seq coll)]
      (cons (f (first s)) (lazy-map f (rest s))))))

By the way, you can abstract the construction of the comparator functions. If we define

(defn comparer [f]
  (fn [x y]
    (println "Comparing with " f)
    (compare (f x) (f y))))

... we can use it to define

(def people-comparision-2 
 (apply compose-comparators (map comparer [:fname :lname :mname])))
like image 113
Thumbnail Avatar answered Oct 04 '22 03:10

Thumbnail


I did some tests with your code and it happens that:

((compose-comparators fname-compare lname-compare mname-compare) person1 person2)

Does work as intended and compares fname only.

According to this blog post, laziness in Clojure might not be enforced as strictly as we might imagine:

Clojure as a language is not lazy by default in totality (unlike Haskell) and hence laziness may get mixed up with strict evaluation leading to surprising and unoptimized consequences.

like image 27
coredump Avatar answered Oct 04 '22 04:10

coredump