2014-09-25 2 views
4

Я читал электронную книгу Functional Programming Patterns in Scala & Clojure и нашел образец кода, который привел к этому вопросу.Имеет ли clojure эквивалент выхода C#?

Этот фрагмент кода предназначен для сравнения двух объектов 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 элемента последовательности за раз по производительности или что-то в этом роде. Каков идиоматический способ Clojure для получения результата/поведения, аналогичного C# -коду?

Следующая попытка. Однако он не чувствует себя «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:

на основе ответов на этот вопрос, а также answer to a related question, я думаю, что если мне нужно ранний выход, я должен быть четко об этом. Один из способов - конвертировать коллекцию в ленивый. Другой вариант - использовать reduced для выхода из цикла сокращения.

Имея знания, которые у меня есть в настоящее время, я более склонен выбирать явный маршрут ленивого сбора. Есть ли проблема с помощью следующей функции, чтобы сделать это -

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

Таким образом, я могу использовать map, remove так, как я обычно имел бы.

+0

FYI 'defn' уже имеет неявный' do' блок в теле. – noisesmith

+0

В 'compose-comparators' вы можете заменить' (filter # (not (zero?%)) 'With' (filter (дополнение zero?)) 'Или' (удалить ноль?) ' – Thumbnail

ответ

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 с chunkiness удалены:

(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]))) 
+0

В примере с ленивой картой, должна ли функция, действующая на 'rest s' быть' map' или 'lazy-map'? Кроме того, разве не лучше сделать коллекцию ленивой, а не просто ленивой? Я не смотрел, но предполагаю, что другие функции коллекции также ведут себя по-разному, если задана последовательность каналов? Хороший рефакторинг на функции сравнения. –

+0

@AmithGeorge 1. Это должно быть «lazy-map» - исправлено. 2. Я не думаю, что это имеет значение, не снимаем ли мы «карту» или коллекцию. Я просто подумал, что первое было проще.'filter' и' remove' сохранить chunkedness тоже. 3. У меня сложилось впечатление, что код в книге часто может быть лучше учтен. Может быть, потому, что конструкции Clojure стоят меньше токенов, чем Scala или C#, что больше факторинга очевидно. – Thumbnail

+0

Я добавил редактирование на свой вопрос. Как вы предположили, нет никакой разницы между unchunking 'map' или' collection', я пошел с созданием ленивой коллекции. Я хотел бы услышать любые комментарии к функции lazy-coll. Если нет, тогда я отмечу это как ответ. –

1

Я сделал несколько тестов с вашим кодом, и это случается, что:

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

ли работы по назначению и сравнивает fname только.

По this blog post, лености в Clojure может не быть исполнена так строго, как мы могли бы себе представить:

Clojure как язык не ленится по умолчанию в совокупности (в отличии от Haskell) и, следовательно, лени может получить смешанное со строгой оценкой , приводящей к неожиданным и неоптимизированным последствиям.

+0

второй работает правильно, потому что он Я думаю, что автор вопроса просто нашел его не очень «clojury». – ponzao

+0

@ponzao Я имел в виду «compose-comparators», я буду редактировать – coredump

+0

@credump, Интересно. В чем разница между тем, что вы опубликовали, и использованием промежуточного значения ?например, - (def people-comparision-2 (compose-comparators fname-compare lname-compare mname-compare)) (people-compareision-2 person1 person2) '... Почему последний печатает выполнение всех трех функций , но первый только первый? –

1

Думаю, вы столкнулись с чередующимися последовательностями. Я не эксперт в этом, но я понимаю, что в зависимости от типа последовательности, которую вы имеете, clojure может оценивать ее в кусках из 32 элементов, а не полностью лениться.

например. Ваш первый код (который не работает, как ожидалось) эффективно:

;; 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 Джорджа ниже)

(some (fn [f] 
     (let [result (f person1 person2)] 
      (if (zero? result) false result))) 
     [c1 c2 c3]) 

;comparing fname 
;14 
+0

Мне пришлось переписать пример 'some' для -' (некоторые # (пусть [результат (% person1 person2)] (if (= result 0) false result)) [c1 c2 c3]) '. Первый будет печатать 0, если первые имена совпадают, а не сравнивать последние/средние имена. Во всяком случае, почему это работает? У меня сложилось впечатление, что «векторы» разделены, а «списки» - нет. Тогда почему используется пример 'some', который использует' vectors'? –

+0

Если вы посмотрите на источник для некоторых (https://github.com/clojure/clojure/blob/03cd9d159a2c49a21d464102bb6d6061488b4ea2/src/clj/clojure/core.clj#L2560), вы можете видеть, что он вызывает fuctions по одному за раз. Независимо от того, реализуются ли значения из seq заданных функций за один пункт за раз или 32 за раз, не имеет значения, поскольку ничего не происходит, пока вы не назовете один из них. Я думаю, что я демонстрирую свое отсутствие полного понимания здесь :) – overthink

+0

Кажется, мне нужно будет перечитать основы Clojure. Это отличается от C#. В зависимости от реализации базовой коллекции или реализации функции, повторяющейся по коллекции, результат может отличаться. Остается вопрос, почему ответ @ coredump ведет себя иначе, чем мой код. Ваше сообщение отвечает на мой вопрос. Хотя я хотел бы оставить вопрос открытым еще некоторое время. Может быть, кто-то мог бы объяснить, почему работает ответ от coredump. –

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 
+0

Спасибо. +1 для того, чтобы ввести меня в 'reduced'. Я всегда ищу примеры идиоматического кода clojure. Лучший способ узнать :) Переходя, я нахожу пример '- >>', данный @overthink, тот, который использует список функций сравнения вместо вектора функций, чтобы быть более читабельным и понятным по назначению. Это не значит, что ваш код не читается или ясен, это, скорее всего, моя зелень в clojure. Любая причина, помимо личных предпочтений, выбирать один над другим? –

+0

Я думаю, что переключение между списком и вектором в контрольное чередование немного эзотерическое, а более надежный подход заключается в явном коротком замыкании, когда короткое замыкание - это требуемое поведение. Тем не менее, его код очень ясен. Существует более четкая версия, использующая сокращение/уменьшение (вероятно, с помощью '- >>'). – noisesmith

+0

Справедливая точка. Исходя из C#, это 'yield' дает мне ленивую последовательность, а также ранний выход. Я ожидал подобного поведения из коллекций и функций clojure. Однако последовательности, выделенные каналами, создали различие. Учитывая, что 'vector', (т.е. коллекция goto clojure's, способ« Список '- коллекция goto C#), фрагментирован, имеет смысл явно возвращаться раньше, а не преобразовывать« вектор »в« список ». @ Thumbnail предложила решение явного преобразования вектора в неклассифицированную ленивую последовательность. Любые мысли по этому поводу? –

Смежные вопросы