Статьи

Clojure Советы от экспертов

Этот первый набор советов от:

Baishampayan Ghose

Найдите его в Твиттере . Его GitHub A / C.

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

Совет № 1 : Сортировка карты по нескольким ключам:

;;; Tip #1
;;; A vector of maps
(def some-maps [{:x 1 :y 2} {:x 2 :y 1} {:x 1 :y 4} {:x 2 :y 8}])

;;; Sort the maps first on 😡 and then on :y

(defn sort-maps-by
"Sort a sequence of maps (ms) on multiple keys (ks)"
[ms ks]
(sort-by #(vec (map % ks)) ms))

;;; (sort-maps-by some-maps [:x :y])
;;; output> ({:x 1, :y 2} {:x 1, :y 4} {:x 2, :y 1} {:x 2, :y 8})

Совет № 2 : Имея дело с бесконечными последовательностями в REPL, вы можете установить количество элементов для печати:

;;; Tip #2
;;; When you type something like (iterate inc 1) on the REPL (or any
;;; kind of infinite, lazy sequence) the REPL will try to evaluate the
;;; whole thing and will never finish. One way to print some parts of
;;; an infinite sequence on the REPL is to do this on the REPL and
;;; then try to print the sequence -
;;; (set! *print-length* 10)
;;; (iterate inc 1)
;;; Which will only print the first 10 items of the above infinite
;;; sequence -
;;; (1 2 3 4 5 6 7 8 9 10 ...)
;;; There is also *print-level* which can be used to determine how
;;; nested/recursive data-structures are printed on the REPL

Совет № 3 : Использование макросов -> & — >> Threading:

Макросы потоков -> & — >> очень полезны, чтобы иногда распутывать вызовы вложенных функций. Макрос -> берет кучу «форм» и «нанизывает их» друг на друга, вставляя каждую форму как второй элемент следующей формы и так далее. Таким образом, (- >> a (bc) (def) (gh)) становится (g (d (bac) ef) h). — >> аналогично, но ставит форму как последний элемент следующей формы. (- >> a (bc) (def) (gh)) затем становится (gh (def (bca))).

(ns tips
;; requires clojure 1.2 if you are on 1.1.x, use this instead
;; (:require [clojure.contrib.duck-streams :as io])
(:require [clojure.contrib.io :as io]))

;;; Tip #3
;;; Use of the -> & ->> threading macros.
(defn word-freq
"Calculate a frequency map of words in a text file."
[f]
(take 20 (->> f
io/read-lines
(mapcat (fn [l] (map #(.toLowerCase %) (re-seq #"\w+" l))))
(remove #{"the" "and" "of" "to" "a" "i" "it" "in" "or" "is"})
(reduce #(assoc %1 %2 (inc (%1 %2 0))) {})
(sort-by (comp - val)))))

;;; Run it like this (word-freq "/path/to/file.txt")

______________________________

Брайан Карпер

Найдите его в Твиттере . Его блог .

Аргументы «именованные» или «ключевые слова» для функций имеют некоторые преимущества по сравнению с позиционными аргументами:

  1. Вы можете указать аргументы в любом порядке.
  2. Аргументы именуются явно, что приводит к меньшему пространству для ошибок по сравнению с позиционными аргументами, где легко перенести два аргумента в списке.
  3. Ваша функция может легко предоставить значения аргументов по умолчанию.

Для функции, которая принимает только один или два аргумента, аргументы ключевого слова могут быть излишними. Но преимущества аргументов ключевых слов становятся более очевидными, чем больше аргументов принимает ваша функция.

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

Первый — просто заставить пользователя явно передать хеш-карту.

(defn named-args-1 [foo argmap]
(println "foo:" foo
"bar:" (:bar argmap 0)
"baz:" (:baz argmap 0))
(println "bar-given?" (contains? argmap :bar)
"baz-given?" (contains? argmap :baz)))

user> (named-args-1 1 {:baz 2})
foo: 1 bar: 0 baz: 2
bar-given? false baz-given? true

Но упаковка аргументов в фигурные скобки, возможно, является ненужным бременем для пользователей вашего кода. Лучший способ — использовать деструктуризацию, чтобы позволить пользователю «сгладить» карту:

(defn named-args-2 [foo & args]
(let [argmap (apply hash-map args)
{:keys [bar baz]
:or {bar 0 baz 0}} argmap]
(println "foo:" foo
"bar:" bar
"baz:" baz)
(println "bar-given?" (contains? argmap :bar)
"baz-given?" (contains? argmap :baz))))

user> (named-args-2 1 :baz 2)
foo: 1 bar: 0 baz: 2
bar-given? false baz-given? true

Это нормально для пользователя, но многословно для автора функций. И список аргументов для функции указывается как «args», что не дает пользователю никакого представления о том, какие ключи ожидаемы или допустимы.

Начиная с последних выпусков Clojure, вы можете выполнить деструктуризацию прямо в списке аргументов функции, что приведет к следующей версии:

(defn named-args-3 [foo & {:keys [bar baz]
:or {bar 0 baz 0}
:as argmap}]
(println "foo:" foo
"bar:" bar
"baz:" baz)
(println "bar-given?" (contains? argmap :bar)
"baz-given?" (contains? argmap :baz)))

user> (named-args-3 1 :baz 2)
foo: 1 bar: 0 baz: 2
bar-given? false baz-given? true

Также можно свернуть свой собственный макрос для использования аргументов ключевых слов. См. Например, clojure.contrib.def / defnk .


Крейг Андера

Найдите его в Твиттере . Его блог .

У меня есть два. Первый, который я украл у Майка Фогуса: использовать «,,,» в качестве заполнителя в макросах -> и — >> . Поскольку запятые являются пробелами, их можно использовать в качестве маркеров, чтобы указать, как выражения проходят через макросы потоков. Так, например, вы можете написать:

(->>
(iterate inc 1)
(map #(* 5 %) ,,,)
(filter odd? ,,,))

и запятые указывают «здесь будет вставлено предыдущее выражение».

Это не то, что вы должны вставить в производственный код, но я обнаружил, что это чрезвычайно полезно при «получении» макросов -> и — >>. Честно говоря, мне нужно было написать это один или два раза, прежде чем он щелкнул со мной, и я вообще перестал использовать запятые.

Другой совет, который я имею, касается понимания того, когда использовать карту , фильтровать и сокращать . Эти три функции являются источником огромной силы Clojure, но я считаю, что новичкам (таким как я) иногда трудно выбрать, какую из них или какую комбинацию использовать. Я обнаружил, что полезно думать об этом с точки зрения того, что у вас есть и что вам нужно:

  • Если у вас есть * последовательность длиной n и вам * нужна * последовательность длины n, используйте map .
  • Если у вас есть * последовательность длиной n и вам * нужна * более короткая последовательность, используйте фильтр .
  • Если у вас * есть * последовательность длины n и вам * нужен * скаляр, используйте уменьшение .

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


Майкель Брандмейер

Найдите его в Твиттере . Его BitBucket Id .

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

Clojure предоставляет множество возможностей для решения сложных задач параллельного программирования. Но все же вы должны понимать семантику базовых объектов. Одним из них являются ссылки , которые позволяют координировать доступ к нескольким различным объектам одновременно. Тем не менее, их использование вызывает немало церемоний. Вы должны вызывать STM машину всякий раз , когда вы хотите написать в иом или хотите последовательный снимок нескольких работ . Также ваша транзакция откатывается в случае повторной попытки транзакции. Это не всегда то, что вы хотите.

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

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

(defn memoize
[f]
(let [cache (atom {})]
(fn [& args]
(when-not (contains? @cache args)
(swap! cache assoc args (apply f args)))
(get @cache args))))

Этот код использует атом и Clojure datastructures, поэтому у нас нет никаких проблем с параллелизмом, верно? Неправильно! Есть много условий гонки между различными вызовами на содержит? , Своп! и получить . В этом примере худшее, что может случиться, — это то, что мы вычисляем значение вызова функции несколько раз. Это уже может быть довольно раздражающим, если вызов дорогостоящ во времени вычислений и / или ресурсах. Но рассмотрим более сложную реализацию кеша, которая также может удалять записи из кеша. Тогда призыв к содержит? мог видеть значение, но когда мы вызываем get, оно может быть уже удалено.

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

Как решить эту проблему? Ну, проблема в том, что мы касаемся атома несколько раз. Таким образом, решение состоит в том, чтобы коснуться атома только один раз!

(defn memoize
[f]
(let [cache (atom {})
update (fn [state args]
(if-not (contains? state args)
(assoc state args (apply f args))
state))]
(fn [& args]
(get (swap! cache update args) args))))

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

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

Примечание . Существуют и другие проблемы, связанные с вышеуказанной проблемой. Например. делать дорогую работу — а именно, звонить е — в обмен! , Пожалуйста, прочитайте сообщение в блоге Meikel на memoize, где еще больше таких соображений принимается во внимание.


Майкл Фогус

Найдите его в Твиттере . Его книга Радость Clojure .

Многие написанные мной макросы запускаются точно так же:

   (defmacro a-macro [& forms]
`'~forms)

Затем он превращается в конвейер, где каждый фрагмент выполняет постепенное преобразование форм:

   (defn do-something [forms]
(frobnicate forms))

(defn do-something-else [forms]
(moidilize forms))

(defmacro a-macro [& forms]
(let [forms (do-something forms)
forms (do-something-else forms)])
`'~forms)

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

Хотя все это довольно загадочно, так как я очень стараюсь избегать написания макросов, иначе меня бьют.


Майкл Коля

Найдите его в Твиттере . Его блог .

Мой совет — это макрос #_ Clojure, который полностью игнорирует следующую форму. Из документов:

«Форма, следующая за #_ , полностью пропущена читателем. (Это более полное удаление, чем макрос комментария, который дает ноль ). »

Это может быть очень полезно при отладке.


Нуруллах Аккая

Найдите его в Твиттере . Его блог .

My tip would be on destructuring, which allows you to pull apart data structures into local bindings.

     (let [[x y] [1 2]]
x)
;;user=> 1

(let [[a b c] "abc"]
c)
;;user=> \c

(let [[[x1 y1][x2 y2]] [[1 2] [3 4]]]
[x1 y1 x2 y2])
;;user=> [1 2 3 4]

Besides destructuring sequential things (vectors, lists, seqs, strings, arrays, or anything that supports nth), you can destructure maps as well:

     (let [{key1 :key1 key2 :key2} {:key1 5 :key2 6}]
[key1 key2])
;;user=> [5 6]

(let [{[x1 y1] :player1 [x2 y2] :player2} {:player1 [5 6] :player2 [9 9]}]
[x1 y1 x2 y2])
;;user=> [5 6 9 9]

Most of the time, your local variables has the same names as the keywords, Clojure provides a shortcut that saves you from typing binding x keyword 😡 over and over again:

     (let [{:keys [key1 key2]} {:key1 5 :key2 6}]
[key1 key2])
;;user=> [5 6]

For more on destructuring, checkout the documentation.


Ramakrishnan Muthukrishnan

Find him on Twitter. His GitHub Id.

Tip #1:

If you have a sequence and want to remove duplicates, there are (atleast) two ways to do it:

(vec (into #{} [1 2 2 3 4 5])) ; => [1 2 3 4 5]

or

(distinct [1 2 2 3 4 5]) ; => [1 2 3 4 5]

The second one is preferred.

Tip #2:

In a function, if you have a list of parameters, you can do the following:

(defn foo [x & xs]
(...))

The same can be done in anonymous functions too. What if you are using the abbreviated form (reader macro form) of an anonymous function? You can still use it by using the “%&” to denote the rest of the argument as a list. One example of the use of this form is shown here.

Tip #3: I echo Craig Andera’s opinions on map, filter and reduce. It is extremely important to master these three constructs. Especially, the way reduce can be used with hash-maps.

Tip #4: If you want to have default values for some of the input parameters, one way is to define functions of diferent arity.

(defn foo
([] (foo "bar"))
([s] (........)))

Here, when ‘foo’ is called without any arguments, we assume a default value of “bar”, a string as argument to the function and call foo with that argument.


Stuart Sierra

Find him on Twitter. His Blog.

Well, I’ve said this before, but it bears saying again: Don’t write a macro where a function will do. Functions are more flexible: they can be composed and passed as values. Do not use macros solely to make the syntax “prettier.”