Поширені помилки програмування для розробників 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
Просто FYI, для тих розробників Clojure, які шукають java.util.Collection # містить функціональність типу, перевірте clojure.contrib.seq-utils / includes? З документації: Використання: (включає? Coll x). Повертає true, якщо coll містить щось рівне (з =) x в лінійний час.
Роберт Кемпбелл,

11
Ви, мабуть, пропустили той факт, що ці питання стосуються Wiki Wiki

3
Мені подобається, як питання Perl просто повинно бути не в курсі всіх інших :)
Ether

8
Для розробників Clojure, які шукають вміст, я б рекомендував не слідувати порадам rcampbell. seq-utils давно застаріло, і ця функція ніколи не була корисною для початку. Ви можете використовувати someфункцію Clojure або, ще краще, просто використовувати її containsсаму. Реалізація колекцій Clojure java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Відповіді:


70

Буквальні октали

Одного разу я читав матрицю, яка використовувала провідні нулі для підтримки належних рядків і стовпців. Математично це правильно, оскільки нуль, очевидно, не змінює базового значення. Спроби визначити var за допомогою цієї матриці, загадково не вдалися б:

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 або 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))))

Зверніть увагу, що я замінив порожній аргумент, тіло функції "конструктор за замовчуванням" (p3 775147 600851475143 3) на цикл + початкове прив'язку. RECUR Тепер виконує повторну прив'язку прив'язок контуру (замість параметрів Fn) і переходить назад до точки рекурсії (петлі, замість Fn).


Посилання на "фантом" вар

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


Ставлення до розуміння списку як до імперативу циклу

По суті, ви створюєте лінивий список на основі існуючих списків, а не просто виконуєте керований цикл. 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")

Також немає перерви або продовжуйте передчасно виїжджати.


Надмірне використання конструкцій

Я походжу з OOPish, тому, коли я починав роботу з 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/

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

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


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


1
Усі макроси зчитування мають нормальну функціональну версію. Ви могли б це зробити (#(hash-set %1 %2) :a 1)або в цьому випадку (hash-set :a 1).
Брайан Карпер

2
Ви також можете "видалити" додаткові дужки з ідентифікацією: (# (ідентичність {% 1% 2}): a 1)

1
Ви також можете використовувати do: (#(do {%1 %2}) :a 1).
Міхал Марчик

@ Міхал - я не люблю це рішення , як багато , як і попередні , тому що робити слід , що побічний ефект відбувається, коли насправді це не той випадок.
Роберт Кемпбелл,

@ rrc7cz: Ну, насправді тут взагалі немає необхідності використовувати анонімну функцію, оскільки використання hash-mapбезпосередньо (як у (hash-map :a 1)або (map hash-map keys vals)) є більш читабельним і не означає, що в іменованій функції щось особливе і ще не реалізоване має місце (що #(...), на мою думку, означає використання ). Насправді, надмірне використання анонімних fns - це задум, про який слід думати сам по собі. :-) ОТО, я іноді використовую doнадто стислі анонімні функції, які не мають побічних ефектів ... Як правило, очевидно, що вони є одним поглядом. Справа смаку, мабуть.
Міхал Марчик

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 змушує оцінювати самі ліниві seqs. Це може ще важче помітити помилку, оскільки ваш код працює на REPL і не працює з вихідного файлу або всередині функції.

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

1
+1. Це мене вкусило, але більш підступно: я оцінював (map ...)зсередини (binding ...)і дивувався, чому нові значення прив'язки не застосовуються.
Alex B,

20

Я клобурський нуб. У досвідчених користувачів можуть виникнути цікавіші проблеми.

намагається надрукувати нескінченні ліниві послідовності.

Я знав, що роблю зі своїми ледачими послідовностями, але для цілей налагодження я вставив кілька викликів print / prn / pr, тимчасово забувши, що це я друкую. Смішно, чому мій ПК весь повісився?

намагаючись запрограмувати Clojure імперативно.

Існує певна спокуса створити цілу кількість refs або atoms і написати код, який постійно блукає з їх станом. Це можна зробити, але це не підходить. Він також може мати низьку продуктивність і рідко отримує користь від кількох ядер.

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

Зворотний бік цього: Деякі алгоритми дійсно хочуть трохи змінюваного стану. Релігійне уникнення будь-якої мінливого стану будь-якою ціною може призвести до повільних або незграбних алгоритмів. Для прийняття рішення потрібні судження та трохи досвіду.

намагається зробити занадто багато в Java.

Оскільки так легко зв’язатися з Java, іноді виникає спокуса використовувати Clojure як обгортку мови сценаріїв навколо Java. Звичайно, вам потрібно буде зробити саме це, використовуючи функціональність бібліотеки Java, але мало сенсу (наприклад, підтримувати структури даних у Java або використовувати типи даних Java, такі як колекції, для яких є добрі еквіваленти в Clojure.


13

Багато речей, про які вже згадувалося. Я просто додаю ще одну.

Clojure, якщо обробляє логічні об'єкти Java завжди як істинне, навіть якщо значення має значення false. Отже, якщо у вас є функція 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"))ні ... Насправді досить заплутано!
Міхал Марчик,

Здається, це працює, як очікувалося в Clojure 1.4.0: (assert (=: false (if Boolean / FALSE: true: false)))
Якуб Холі,

Я також нещодавно опікся цим (фільтр: mykey coll) де: значення mykey, де Booleans - працює, як очікувалося, зі створеними Clojure колекціями, але НЕ з десеріалізованими колекціями, коли серіалізується за допомогою серіалізації Java за замовчуванням - тому що ці Booleans десеріалізовані як new Boolean (), і на жаль (new Boolean (true)! = java.lang.Boolean / TRUE)
Hendekagon

1
Просто пам’ятайте основні правила булевих значень у Clojure - nilі falseвони є помилковими, а все інше - істиною. Java Booleanне є nilі не є false(оскільки вона є об'єктом), тому поведінка є послідовною.
erikprice

13

Тримаючи голову в петлях.
Ви ризикуєте втратити пам’ять, якщо прокручуєте елементи потенційно дуже великої або нескінченної лінивої послідовності, зберігаючи посилання на перший елемент.

Забувши, що немає TCO.
Регулярні зворотні дзвінки забирають місце у стеку, і вони будуть переповнюватися, якщо ви не будете обережні. Clojure розглядає 'recurі 'trampolineрозглядає багато випадків, коли оптимізовані зворотні дзвінки будуть використовуватися іншими мовами, але ці методи повинні бути навмисно застосовані.

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

Внесення змінних речей у посилання.
Ви можете технічно це зробити, але STM регулює лише посилання на об'єкт у самому посиланні, а не на об'єкт, що посилається, та на його поля (якщо вони не є незмінними та не вказують на інші посилання). Тому, коли це можливо, віддайте перевагу лише незмінним об’єктам у посиланнях. Те саме стосується атомів.


4
майбутня гілка розробки проходить довгий шлях до зменшення першого елемента шляхом стирання посилань на об'єкти у функції, як тільки вони стають недосяжними локально.
Arthur Ulfeldt

9

використання loop ... recurдля обробки послідовностей, коли це буде робити карта.

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

проти

(map do-stuff data)

Функція map (в останній гілці) використовує фрагментовані послідовності та багато інших оптимізацій. Крім того, оскільки ця функція часто використовується, Hotspot JIT зазвичай має її оптимізовану та готову до роботи, не виходячи з "часу розминки".


1
Ці дві версії насправді не є еквівалентними. Ваша workфункція еквівалентна (doseq [item data] (do-stuff item)). (Окрім того, що ця петля в роботі ніколи не закінчується.)
котарак

так, перший порушує лінь на своїх аргументах. отримані seq матимуть однакові значення, хоча це вже не буде ледаче seq.
Артур Ульфельдт,

+1! Я написав численні невеликі рекурсивні функції, лише для того, щоб знайти ще один день, коли їх можна було б узагальнити за допомогою mapта / або reduce.
nperson325681

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]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.