Общие ошибки программирования для разработчиков Clojure, чтобы избежать

Каковы некоторые распространенные ошибки разработчиков Clojure и как их избежать?

Например; новички на Clojure считают, что функция contains? работает так же, как java.util.Collection#contains. Тем не менее, contains? будет работать аналогичным образом при использовании с индексированными коллекциями, такими как карты и наборы, и вы ищете данный ключ:

(contains? {:a 1 :b 2} :b)
;=> true
(contains? {:a 1 :b 2} 2)
;=> false
(contains? #{:a 1 :b 2} :b)
;=> true

При использовании с численно индексированными коллекциями (векторы, массивы) contains? только проверяет, что данный элемент находится в допустимом диапазоне индексов (на основе нуля):

(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true

Если задан список, contains? никогда не вернет true.

Ответ 1

Буквенные символы

В какой-то момент я читал в матрице, которая использовала ведущие нули для поддержания правильных строк и столбцов. Математически это правильно, так как начальный нуль, очевидно, не меняет базового значения. Однако попытки определить var с этой матрицей таинственно исчезли:

java.lang.NumberFormatException: Invalid number: 08

что полностью сбило меня с толку. Причина в том, что Clojure обрабатывает буквальные целочисленные значения с ведущими нулями как восьмеричные, а восьмеричное число не равно 8.

Я также должен отметить, что Clojure поддерживает традиционные шестнадцатеричные значения Java через префикс 0x. Вы также можете использовать любое основание между 2 и 36, используя нотацию "base + r + value", например 2r101010 или 36r16, которые составляют 42 базовые десять.


Попытка вернуть литералы в анонимный литерал функции

Это работает:

user> (defn foo [key val]
    {key val})
#'user/foo
user> (foo :a 1)
{:a 1}

поэтому я полагал, что это также сработает:

(#({%1 %2}) :a 1)

но сбой:

java.lang.IllegalArgumentException: Wrong number of args passed to: PersistentArrayMap

потому что макрос читателя #() расширяется до

(fn [%1 %2] ({%1 %2}))  

с символом карты, заключенным в скобки. Так как это первый элемент, он рассматривается как функция (на самом деле это буквальная карта), но не требуются необходимые аргументы (такие как ключ). Таким образом, литерал анонимной функции не расширяется до

(fn [%1 %2] {%1 %2})  ; notice the lack of parenthesis

и поэтому вы не можете иметь буквальное значение ([],: a, 4,%) как тело анонимной функции.

В комментариях были даны два решения. Брайан Карпер предлагает использовать конструкторы реализации последовательностей (массив-карта, хэш-набор, вектор) так:

(#(array-map %1 %2) :a 1)

а Дэн показывает, что вы можете использовать функцию identity, чтобы развернуть внешние скобки:

(#(identity {%1 %2}) :a 1)

Предложение Брайана на самом деле приводит меня к следующей ошибке...


Думая, что hash-map или array-map определить неизменную реализацию конкретной карты

Рассмотрим следующее:

user> (class (hash-map))
clojure.lang.PersistentArrayMap
user> (class (hash-map :a 1))
clojure.lang.PersistentHashMap
user> (class (assoc (apply array-map (range 2000)) :a :1))
clojure.lang.PersistentHashMap

В то время как вам вообще не нужно беспокоиться о конкретной реализации карты Clojure, вы должны знать, что функции, которые выражают карту - например assoc или conj - может принимать PersistentArrayMap и возвращать PersistentHashMap, который быстрее работает для больших карт.


Использование функции как точки рекурсии, а не цикла для обеспечения начальных привязок

Когда я начал, я написал много таких функций:

; Project Euler #3
(defn p3 
  ([] (p3 775147 600851475143 3))
  ([i n times]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

Если на самом деле цикл был бы более кратким и идиоматичным для этой конкретной функции:

; Elapsed time: 387 msecs
(defn p3 [] {:post [(= % 6857)]}
  (loop [i 775147 n 600851475143 times 3]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

Обратите внимание, что я заменил пустой аргумент, конструктор "default constructor" body (p3 775147 600851475143 3) с циклом + начальное связывание. recur теперь восстанавливает привязки к циклу (вместо параметров fn) и возвращается к точке рекурсии (цикл вместо fn).


Ссылка на phantom "vars

Я говорю о типе var, который вы можете определить с помощью REPL - во время вашего поискового программирования, а затем бессознательно ссылайтесь в своем источнике. Все работает нормально, пока вы не перезагрузите пространство имен (возможно, закрыв редактор), а затем обнаружите кучу несвязанных символов, на которые ссылается весь ваш код. Это также часто происходит при рефакторинге, перемещая переменную из одного пространства имен в другое.


Рассмотрение для понимания списка как императив для цикла

По существу, вы создаете ленивый список на основе существующих списков, а не просто выполняете управляемый цикл. Clojure doseq на самом деле больше похож на императивные конструкторы цикла foreach.

Одним из примеров того, как они отличаются, является способность фильтровать элементы, которые они перебирают, используя произвольные предикаты:

user> (for [n '(1 2 3 4) :when (even? n)] n)
(2 4)

user> (for [n '(4 3 2 1) :while (even? n)] n)
(4)

Другим способом они отличаются друг от друга: они могут работать с бесконечными ленивыми последовательностями:

user> (take 5 (for [x (iterate inc 0) :when (> (* x x) 3)] (* 2 x)))
(4 6 8 10 12)

Они также могут обрабатывать более одного обязательного выражения, сначала итерации над самым правым выражением, и работа его влево:

user> (for [x '(1 2 3) y '(\a \b \c)] (str x y))
("1a" "1b" "1c" "2a" "2b" "2c" "3a" "3b" "3c")

Там также нет break или продолжить, чтобы выйти преждевременно.


Избыточное использование структур

Я родом из OOPish, поэтому, когда я начал Clojure, мой мозг все еще думал об объектах. Я обнаружил, что моделирую все как struct, потому что его группировка "членов", как бы они ни были свободными, заставила меня чувствовать себя комфортно. На самом деле structs следует в основном рассматривать как оптимизацию; Clojure будет делиться ключами и некоторой информацией поиска для сохранения памяти. Вы также можете оптимизировать их, указав accessors, чтобы ускорить процесс поиска ключей.

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


Использование unsugared конструкторов BigDecimal

Мне понадобилось много BigDecimals и писал уродливый код следующим образом:

(let [foo (BigDecimal. "1") bar (BigDecimal. "42.42") baz (BigDecimal. "24.24")]

когда на самом деле Clojure поддерживает BigDecimal литералы, добавляя M к числу:

(= (BigDecimal. "42.42") 42.42M) ; true

Использование sugared версии вырезает много наворотов. В комментариях twils упоминалось, что вы также можете использовать bigdec и bigint функции более ясны, но остаются краткими.


Использование преобразования имен пакетов Java для пространств имен

На самом деле это не ошибка, а нечто, что противоречит идиоматической структуре и названию типичного проекта Clojure. Мой первый существенный проект Clojure имел декларации пространства имен - и соответствующие структуры папок - вот так:

(ns com.14clouds.myapp.repository)

который раздувал мои полностью квалифицированные ссылки на функции:

(com.14clouds.myapp.repository/load-by-name "foo")

Чтобы усложнить ситуацию, я использовал стандартную структуру каталогов Maven:

|-- src/
|   |-- main/
|   |   |-- java/
|   |   |-- clojure/
|   |   |-- resources/
|   |-- test/
...

который является более сложным, чем стандартная структура Clojure:

|-- src/
|-- test/
|-- resources/

который по умолчанию является Leiningen и Clojure сам.


Карты используют Java equals(), а не Clojure= для сопоставления клавиш

Первоначально сообщается chouser на IRC, это использование Java equals() приводит к некоторым неинтуитивным результатам:

user> (= (int 1) (long 1))
true
user> ({(int 1) :found} (int 1) :not-found)
:found
user> ({(int 1) :found} (long 1) :not-found)
:not-found

Поскольку оба экземпляра Целое и Длинный 1 печатаются по умолчанию одинаково, может быть сложно определить, почему ваша карта не возвращает никаких значений. Это особенно верно, когда вы передаете свой ключ через функцию, которая, возможно, без ведома вам, возвращает длинный.

Следует отметить, что использование Java equals() вместо Clojure = имеет важное значение для соответствия карт интерфейсу java.util.Map.


Я использую Программирование Clojure Стюарта Хэллоуэй, Практическое Clojure от Luke VanderHart и помощь бесчисленных хакеров Clojure на IRC и список рассылки, чтобы помочь мои ответы.

Ответ 2

Забытие принудительной оценки ленивых секций

Lazy seqs не оцениваются, если вы не попросите их оценить. Вы можете ожидать, что это что-то напечатает, но это не так.

user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil

map никогда не оценивается, он молча отбрасывается, потому что он ленив. Вы должны использовать один из doseq, dorun, doall и т.д. Для принудительной оценки ленивых последовательностей для побочных эффектов.

user=> (defn foo [] (doseq [x [:foo :bar]] (println x)) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil
user=> (defn foo [] (dorun (map println [:foo :bar])) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil

Использование bare map в стиле REPL выглядит так, как будто оно работает, но оно работает только потому, что REPL принудительно оценивает ленивые seqs. Это может сделать ошибку еще труднее заметить, потому что ваш код работает в REPL и не работает из исходного файла или внутри функции.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)

Ответ 3

Я Clojure noob. Более продвинутые пользователи могут иметь более интересные проблемы.

пытается напечатать бесконечные ленивые последовательности.

Я знал, что делаю с ленивыми последовательностями, но для целей отладки я вставил некоторые вызовы print/prn/pr, временно забыв, что именно я печатаю. Забавно, почему мой компьютер повесил трубку?

попытка запрограммировать Clojure императивно.

Существует некоторое искушение создать целую коллекцию ref или atom и написать код, который постоянно галочит с их состоянием. Это можно сделать, но это не очень удобно. Он также может иметь низкую производительность и редко извлекать выгоду из нескольких ядер.

пытается запрограммировать Clojure на 100% функционально.

Перевернутая сторона: некоторые алгоритмы действительно хотят немного изменчивого состояния. Религиозное избегание изменчивого состояния любой ценой может привести к медленным или неудобным алгоритмам. Он принимает решение и немного опыта, чтобы принять решение.

пытается сделать слишком много в Java.

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

Ответ 4

Сохранение головы в цикле.
Вы рискуете исчерпать память, если будете перебирать элементы потенциально очень большой или бесконечной ленивой последовательности, сохраняя ссылку на первый элемент.

Забыть там нет TCO.
Регулярные хвостовые вызовы потребляют пространство стека, и они будут переполняться, если вы не будете осторожны. Clojure имеет 'recur и 'trampoline для обработки многих случаев, когда оптимизированные обратные вызовы будут использоваться на других языках, но эти методы должны быть намеренно применены.

Не-ленивые последовательности.
Вы можете создать ленивую последовательность с 'lazy-seq или 'lazy-cons (или путем создания более ленивых API-интерфейсов более высокого уровня), но если вы оберните ее в 'vec или передадите ее через какую-то другую функцию, которая реализует последовательность, то она не будет дольше быть ленивым. И стек, и куча могут быть переполнены этим.

Включение измененных вещей в ссылки.
Вы можете технически это сделать, но только ссылка на объект в самом рефере определяется STM - не упомянутым объектом и его полями (если они не являются неизменными и указывают на другие ссылки). Поэтому, когда это возможно, предпочитайте только неподвижные объекты в refs. То же самое происходит и для атомов.

Ответ 5

Многое уже упоминалось. Я просто добавлю еще один.

Clojure , если обрабатывает объекты Java Boolean всегда как истинные, даже если значение равно false. Поэтому, если у вас есть функция java land, которая возвращает значение java Boolean, убедитесь, что вы не проверяете его напрямую  (if java-bool "Yes" "No") скорее (if (boolean java-bool) "Yes" "No").

Я обжегся этим с помощью библиотеки clojure.contrib.sql, которая возвращает логические поля базы данных в виде java-булевых объектов.

Ответ 6

с помощью loop ... recur для обработки последовательностей, когда карта будет делать.

(defn work [data]
    (do-stuff (first data))
    (recur (rest data)))

против.

(map do-stuff data)

Функция карты (в последней ветки) использует чередующиеся последовательности и многие другие оптимизации. Кроме того, поскольку эта функция часто запускается, Hotspot JIT обычно оптимизирован и готов отказаться от любого "разогрева".

Ответ 7

Типы коллекций имеют разные типы поведения для некоторых операций:

user=> (conj '(1 2 3) 4)    
(4 1 2 3)                 ;; new element at the front
user=> (conj [1 2 3] 4) 
[1 2 3 4]                 ;; new element at the back

user=> (into '(3 4) (list 5 6 7))
(7 6 5 3 4)
user=> (into [3 4] (list 5 6 7)) 
[3 4 5 6 7]

Работа со строками может сбивать с толку (я все еще не совсем понимаю их). В частности, строки не совпадают с последовательностями символов, даже если над ними работают функции последовательности:

user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)

Чтобы получить строку обратно, вам нужно будет сделать:

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"

Ответ 8

слишком много скобок, особенно с вызовом метода void java внутри, что приводит к NPE:

public void foo() {}

((.foo))

приводит к NPE из внешних скобок, потому что внутренние скобки оцениваются до нуля.

public int bar() { return 5; }

((.bar)) 

приводит к упрощению отладки:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]