Распространенные ошибки программирования, которых следует избегать разработчикам Clojure [закрыто]

92

Какие типичные ошибки допускают разработчики 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?никогда не вернет истину.

туман
источник
4
К вашему сведению, для тех разработчиков Clojure, которые ищут java.util.Collection # contains type functions, проверьте clojure.contrib.seq-utils / includes? Из документов: Использование: (включает? Coll x). Возвращает истину, если coll содержит что-то равное (с =) x за линейное время.
Роберт Кэмпбелл
11
Вы, кажется, упустили тот факт, что эти вопросы относятся к Вики Сообщества
3
Мне нравится, что вопрос Perl просто должен не совпадать со всеми остальными :)
Ether
8
Разработчикам Clojure, которые ищут контейнеры, я бы рекомендовал не следовать совету rcampbell. seq-utils давно устарела, и эта функция никогда не была полезной. Вы можете использовать someфункцию Clojure или, что еще лучше, просто использовать containsсаму себя. Реализация коллекций Clojure java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Ответы:

70

Буквальные восьмеричные числа

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

java.lang.NumberFormatException: Invalid number: 08

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

Я также должен упомянуть, что Clojure поддерживает традиционные шестнадцатеричные значения Java через префикс 0x . Вы также можете использовать любое основание от 2 до 36, используя обозначение «основание + r + значение», например, 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 {%1 %2}) :a 1)

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


Думая, что хеш-карта или массив-карта определяют неизменную реализацию конкретной карты

Обратите внимание на следующее:

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 или conc - могут принимать 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))))

Обратите внимание, что я заменил пустой аргумент, тело функции «конструктор по умолчанию» (p3 775147 600851475143 3) на цикл + начальное связывание. Теперь повторение связывает привязки цикла (вместо параметров fn) и переходит обратно к точке рекурсии (loop вместо fn).


Ссылка на "фантомные" вары

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


Обработка понимания списка for как императива цикла for

По сути, вы создаете ленивый список на основе существующих списков, а не просто выполняете управляемый цикл. DoSq в Clojure на самом деле больше аналогичен императивным конструкциям цикла 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")

Также нет перерыва или продолжения преждевременного выхода.


Чрезмерное использование структур

У меня опыт работы с ООП, поэтому, когда я начал Clojure, мой мозг все еще думал об объектах. Я обнаружил, что моделирую все как структуру, потому что ее группировка «членов», какими бы свободными они ни были, заставляла меня чувствовать себя комфортно. На самом деле, структуры следует рассматривать как оптимизацию; Clojure поделится ключами и некоторой поисковой информацией для экономии памяти. Вы можете дополнительно оптимизировать их, указав средства доступа для ускорения процесса поиска ключей.

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


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

Мне нужно было много BigDecimals, и я писал уродливый код вроде этого:

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

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

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

Использование засахаренной версии избавляет от вздутия живота. В комментариях 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 как Integer, так и Long печатаются по умолчанию одинаково, может быть сложно определить, почему ваша карта не возвращает никаких значений. Это особенно верно, когда вы передаете ключ через функцию, которая, возможно, без вашего ведома, возвращает long.

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


Я использую Programming Clojure Стюарта Хэллоуэя, Practical Clojure Люка Вандерхарта, а также помощь бесчисленных хакеров Clojure в IRC и списке рассылки, чтобы помочь в моих ответах.

rcampbell
источник
1
Все макросы считывателя имеют нормальную функциональную версию. Вы могли бы сделать (#(hash-set %1 %2) :a 1)или в этом случае (hash-set :a 1).
Брайан Карпер,
2
Вы также можете «удалить» дополнительные круглые скобки с помощью идентификатора: (# (identity {% 1% 2}): a 1)
1
Вы также можете использовать do: (#(do {%1 %2}) :a 1).
Michał Marczyk
@ Michał - мне не нравится это решение так сильно, как предыдущие, потому что do подразумевает, что имеет место побочный эффект, хотя на самом деле это не так.
Роберт Кэмпбелл
@ rrc7cz: Ну, на самом деле здесь вообще нет необходимости использовать анонимную функцию, поскольку использование hash-mapнапрямую (как в (hash-map :a 1)or (map hash-map keys vals)) более читабельно и не означает, что что-то особенное и пока еще не реализованное в именованной функции происходит (что #(...), как я считаю, подразумевает использование ). Фактически, чрезмерное использование анонимных fns - это ошибка, о которой нужно думать само по себе. :-) OTOH, я иногда использую doв сверхкоротких анонимных функциях, которые не имеют побочных эффектов ... Кажется очевидным, что они доступны с первого взгляда. Думаю, дело вкуса.
Michał Marczyk
42

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

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

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

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

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
Брайан Карпер
источник
1
+1. Это укусило меня, но более коварно: я оценивал (map ...)изнутри (binding ...)и задавался вопросом, почему новые значения привязки не применяются.
Alex B
20

Я новичок в Clojure. У более продвинутых пользователей могут быть более интересные задачи.

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

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

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

Есть некоторый соблазн создать множество refs или atoms и написать код, который постоянно портит их состояние. Это можно сделать, но это не подходит. Он также может иметь низкую производительность и редко использует несколько ядер.

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

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

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

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

Карл Смотрич
источник
13

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

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

Меня обожгло это с библиотекой clojure.contrib.sql, которая возвращает логические поля базы данных в виде логических объектов java.

Вагиф Верди
источник
8
Обратите внимание, что (if java.lang.Boolean/FALSE (println "foo"))не печатает foo. (if (java.lang.Boolean. "false") (println "foo"))хотя (if (boolean (java.lang.Boolean "false")) (println "foo"))и не делает ... Действительно, довольно запутанно!
Michał Marczyk
Похоже, что в Clojure 1.4.0 это работает, как ожидалось: (assert (=: false (if Boolean / FALSE: true: false)))
Якуб Холи
Я также недавно сгорел от этого, когда делал (filter: mykey coll) where: mykey's values ​​where Booleans - работает должным образом с коллекциями, созданными Clojure, но НЕ с десериализованными коллекциями, когда сериализуется с использованием сериализации Java по умолчанию - потому что эти логические значения десериализованы как new Boolean (), и, к сожалению (new Boolean (true)! = java.lang.Boolean / TRUE)
Hendekagon
1
Просто запомните основные правила логических значений в Clojure - nilи falseложны, а все остальное верно. Java Booleanне существует, nilи это не так false(потому что это объект), поэтому поведение согласовано.
erikprice
13

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

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

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

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

Крис Вест
источник
4
предстоящая ветвь разработки идет долгий путь к сокращению первого элемента, удаляя ссылки на объекты в функции, когда они становятся локально недоступными.
Артур Ульфельдт
9

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

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

vs.

(map do-stuff data)

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

Артур Ульфельдт
источник
1
Эти две версии на самом деле не эквивалентны. Ваша workфункция эквивалентна (doseq [item data] (do-stuff item)). (Помимо того факта, что этот цикл в работе никогда не заканчивается.)
Котарак
да, первый ломает лень своими аргументами. результирующий seq будет иметь те же значения, но больше не будет ленивым seq.
Артур Ульфельдт
+1! Я написал множество небольших рекурсивных функций только для того, чтобы найти еще один день, когда все это можно было бы обобщить с помощью mapи / или reduce.
mike3996
5

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

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"
Мэтт Фенвик
источник
3

слишком много скобок, особенно с вызовом метода 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]
Miaubiz
источник