Статьи

Читаемый Clojure без Java-эквивалента?

Я недавно присоединился к новой команде, и мы сделали немного Clojure. Если вы никогда не работали с Лиспом (а я не делал до того, как нашел Clojure), то естественно задать вопрос: будет ли когда-нибудь естественным программирование на Лиспе, особенно префиксная нотация ? Вопрос возник несколько недель назад, и у меня было два ответа.

Прежде всего, я никогда не расстраивался из-за скобок. На самом деле, в эти дни я немного занимался Java и не вижу большой разницы между verify (foo) .bar (eq (100), any (Baz.class), eq («Cat»)) и (-> foo verify (.bar (eq 100) (any Baz) (eq «Cat))). По моим подсчетам это такое же количество скобок. Место их расположения немного меняется, но я не считаю, это будет хорошо или плохо.

Люди также любят приводить такие примеры:

(apply merge-with +
(pmap count-lines
(partition-all *batch-size*
(line-seq (reader filename)))))

Стефан Тилков говорил об этом в предыдущей
записи в блоге , и (в комментариях к записи Стефана) Рич Хики отмечает, что вы можете использовать несколько разных версий, если вы предпочитаете выкладывать свой код по-другому. Ниже представлены два альтернативных решения Rich.

; Тот же код в конвейерном стиле Clojure:

(->> (line-seq (reader filename))
(partition-all *batch-size*)
(pmap count-lines)
(apply merge-with +))

; и в пошаговом стиле Clojure с помеченными промежуточными результатами (комментарий Адриана):

(let [lines (line-seq (reader filename))
processed-data (pmap count-lines
(partition-all *batch-size* lines))]
(apply merge-with + processed-data))

Некоторое время я чувствовал то же самое. Вы можете написать Cobol на любом языке. Вопрос в том, готовы ли вы научиться решать проблемы элегантно и идиоматически на новом языке. Если вы готовы потратить время, вы сможете сами выяснить, насколько естественно писать идиоматический код Clojure.

Это был мой ответ до сегодняшнего дня. Работая над некоторым кодом Java, я наткнулся на следующий метод Java.

public void onFill(int fillQty) {
this.qty = Math.max(this.qty - fillQty, 0);
}

Это простой Java-метод, который уменьшает состояние незавершенного количества заказа на сумму только что выполненного заказа. Читая строчку, я не мог не чувствовать, что должен быть более элегантный способ выразить логику. Вы хотите установить состояние непогашенного количества на текущее непогашенное количество минус только что заполненное количество, но вы также никогда не хотите, чтобы непогашенное количество опустилось ниже нуля. Я читал слева направо, и я действительно хотел, чтобы выразить эту логику таким образом, чтобы следовать схеме слева направо.

В Clojure это легко сделать:

(swap! qty #(-> % (- fill-qty) (Math/max 0)))

Для читателей, которые менее знакомы с макросом считывателя Clojure
, приведенный выше пример также можно записать так:

(swap! qty (fn [current-qty] (-> current-qty (- fill-qty) (Math/max 0))))

В приведенном выше примере своп! устанавливает состояние qty с возвращаемым значением функции.

Если вы действительно новичок в Clojure, это может быть слишком много, поэтому мы можем уменьшить пример и удалить настройку состояния. Вот версия на Java, которая игнорирует состояние установки.

Math.max(this.qty - fillQty, 0);

Пример ниже является логическим эквивалентом в Clojure.

(-> qty (- fill-qty) (Math/max 0))

При чтении приведенного выше примера Java я вынужден поместить Math.max в свой умственный стек, оценить this.qty — fillQty, а затем мысленно оценить метод, который я положил в стек, с моим новым результатом и дополнительными аргументами. Это не ракетостроение, но и не то, как я читаю (слева направо). С другой стороны, когда я читаю версию Clojure, я думаю — взять текущее количество, вычесть количество заполнения, затем взять максимум этого и ноль. Код читается небольшими логическими фрагментами, которые мне легко переварить.

Очевидно, что код Java можно также переписать несколькими другими способами. Вот пример кода Java, который читает слева направо и сверху вниз.

public void onFill(int fillQty) {
this.qty -= fillQty
this.qty = Math.max(this.qty, 0);
}

И я могу сделать нечто подобное в Clojure, если захочу.

(swap! qty #(- % fill-qty))
(swap! qty #(Math/max % 0))

Хотя можно написать Clojure аналогично приведенному выше примеру, гораздо более вероятно, что вы бы использовали оператор let, если хотите разбить две операции.

(defn update-qty [current fill-qty]
(let [total-qty (- current fill-qty)]
(Math/max total-qty 0)))

(swap! qty update-qty fill-qty)

Приведенный выше пример, вероятно, примерно эквивалентен следующему фрагменту Java.

public void onFill(int fillQty) {
int newTotalQty = this.qty - fillQty
this.qty = Math.max(newTotalQty, 0);
}

Итак, я могу написать код, который похож на мои параметры в Java, но мне все еще не хватает версии Java, которая похожа на этот пример Clojure:

(swap! qty #(-> % (- fill-qty) (Math/max 0)))

Единственное, что приходит на ум, это какой-то плавный интерфейс, который позволяет мне сказать this.qty = this.qty.minus (fillQty) .maxOfIdentityOrZero (), но я не могу придумать реалистичный способ создания этого API без довольно много кода инфраструктуры (включая мой собственный класс Integer).

(обратите внимание, вы можете расширить Integer на языке с открытыми классами, но это выходит за рамки этого обсуждения)

Последний пример Clojure определенно является версией кода, который я бы предпочел. Мои предпочтения основаны на том, как код читает краткие логические фрагменты слева для записи. Мне не нужно разбираться наизнанку, как заставляет меня исходная версия Java, и мне не нужно разбивать свою работу на две строки.

Я уверен, что есть ситуации, когда Java позволил мне создать элегантное решение, которое было бы невозможно в Clojure. Эта запись не предназначена для отправки сообщения «Clojure лучше Java». Я не верю этому. Тем не менее, до сегодняшнего дня я придерживался мнения, что вы можете написать Clojure, который логически разбивает проблемы так, как это делается в Java. Тем не менее, теперь я также расширил свое мнение, включив в него тот факт, что в определенных ситуациях Clojure также может позволить мне решать проблемы способом, который я считаю превосходящим мои возможности в Java.

И, да, после некоторого времени привыкания к синтаксису Lisp, для меня определенно совершенно естественно, когда я разрабатываю с использованием Clojure.

 

From http://blog.jayfields.com/2011/03/readable-clojure-without-java.html