Те, кто не знаком с Clojure, часто интересуются тем, как вы управляете изменением состояния в своих приложениях. Если вы слышали кое-что о Clojure, но на самом деле не смотрели на него, я не удивлюсь, если вы подумали, что невозможно написать «настоящее» приложение с Clojure, поскольку «все неизменно». Я даже слышал, что разработчик, которого я уважаю, ошибается, говоря: мы не собираемся использовать Clojure, потому что он плохо обрабатывает состояние.
Очевидно, что государственное управление в Clojure очень неправильно понято.
На самом деле мне было трудно не назвать эту запись в блоге «Clojure, это о состоянии». Я думаю, что государство формирует Clojure больше, чем любое другое влияние; это ядро языка (насколько я могу судить).Рич Хики явно потратил много времени на размышления о состоянии — здесь есть эссеhttp://clojure.org/state, который описывает типичные проблемы с традиционным подходом к управлению государством и решениями Clojure.
Эссе Рича хорошо описывает его взгляды на состояние; Вы должны прочитать его, прежде чем продолжить эту запись. В оставшейся части этой записи будут приведены примеры того, как вы можете управлять состоянием, используя функции Clojure.
В конце эссе Рича он говорит:
В локальном случае, поскольку Clojure не имеет изменяемых локальных переменных, вместо создания значений в мутирующем цикле вы можете вместо этого делать это функционально с помощью recur или Reduce.
Прежде чем мы начнем сокращаться, давайте начнем с самого простого примера. У вас есть массив целых чисел, и вы хотите удвоить каждое целое число. В языке с изменяемым состоянием вы можете перебирать массив и создавать новый массив с каждым удвоенным целым числом.
for (int i=0; i < nums.length; i++) {
result.add(nums[i] * 2);
}
В Clojure вы создадите новый массив, вызвав
функцию map с функцией, которая удваивает каждое значение. (Я использую Clojure 1.2)
user=> (map (fn [i] (* i 2)) [1 2 3]) (2 4 6)
Если вы новичок в Clojure, стоит упомянуть несколько вещей. «user =>» — это
приглашение REPL . Вы вводите некоторый текст и нажимаете ввод, и текст оценивается. Если вы завершили список (закрыли скобки), результаты оценки этого списка будут напечатаны в следующей строке.
Я помню то, о чем думал в первый раз, когда посмотрел на шутку, и я знаю, что код может не выглядеть как читаемый код, поэтому вот версия, которая разбивает некоторые из понятий и может упростить усвоение примера.
user=> (defn double-int [i] (* i 2)) #'user/double-int user=> (def the-array [1 2 3]) #'user/the-array user=> (map double-int the-array) (2 4 6)
В первом примере с Clojure вы вызываете
функцию fn для создания анонимной функции, которая затем передается в функцию map (для применения к каждому элементу массива). Функция map — это функция
высокого порядка, которая может принимать анонимную функцию (пример 1) или именованную функцию (double-int, пример 2). В Clojure (def …) — это
специальная форма, которая позволяет вам определять переменную, а
defn — это функция, которая позволяет вам легко определить функцию и назначить ее для переменной. Синтаксис defn довольно прост, первый аргумент — это имя, второй аргумент — список аргументов новой функции, а любые дополнительные формы — это тело функции, которую вы определяете.
Как только вы привыкнете к синтаксису Clojure, вы даже можете немного позабавиться с именами функций, что может привести к сжатому и понятному коду.
user=> (defn *2 [i] (* 2 i)) #'user/*2 user=> (map *2 [1 2 3]) (2 4 6)
но я отвлекся.
Точно так же вы можете суммировать числа из массива.
for (int i = 0; i < nums.length; i++) { result += nums[i]; }
Вы можете достичь цели сокращения массива в одно значение в Clojure с помощью
снижения функции.
user=> (reduce + [1 2 3]) 6
Clojure имеет несколько функций, которые позволяют вам создавать новые значения из существующих значений, которых должно быть достаточно для решения любой проблемы, в которой вы бы традиционно использовали локальные изменяемые переменные.
Для нелокального изменяемого состояния у вас обычно есть 3 варианта:
атомы ,
ссылки и
агенты .
Когда я начал программировать в Clojure, атомы были моим основным выбором для изменяемого состояния. Атомы очень просты в использовании и требуют только нескольких функций для взаимодействия с ними. Давайте предположим, что мы создаем торговое приложение, которое должно соответствовать текущей цене Apple. Наше приложение будет вызывать нашу функцию apple-price-update, когда будет получена новая цена, и нам нужно будет сохранить эту цену для (возможного) дальнейшего использования. В приведенном ниже примере показано, как можно использовать атом для отслеживания текущей цены Apple.
user=> (def apple-price (atom nil)) #'user/apple-price user=> (defn update-apple-price [new-price] (reset! apple-price new-price)) #'user/update-apple-price user=> @apple-price nil user=> (update-apple-price 300.00) 300.0 user=> @apple-price 300.0 user=> (update-apple-price 301.00) 301.0 user=> (update-apple-price 302.00) 302.0 user=> @apple-price 302.0
Приведенный выше пример демонстрирует, как вы можете создать новый атом и сбрасывать его значение при каждом обновлении цены.
Сброса! Функция устанавливает значение атома синхронно и возвращает его новое значение. Вы также можете запросить цену на яблоко в любое время, используя
@ (или deref) .
Если вы работаете с Java-фоном, пример, приведенный выше, должен быть наиболее простым для связи. Каждый раз, когда мы вызываем функцию update-apple-price, наше состояние устанавливается в новое значение. Однако атомы предоставляют гораздо большую ценность, чем просто переменная, которую вы можете сбросить.
Возможно, вы помните следующий пример из
Java Concurrency in Practice .
@NotThreadSafe public class UnsafeSequence { private int value; /** * Returns a unique value. */ public int getNext() { return value++; } }
Книга объясняет, почему это может вызвать потенциальные проблемы.
Проблема с UnsafeSequence заключается в том, что при некоторой неудачной синхронизации два потока могут вызвать getNext и получить одно и то же значение. Нотация приращения nextValue ++ может показаться одной операцией, но на самом деле это три отдельные операции: прочитать значение, добавить к нему одну и записать новое значение. Поскольку операции в нескольких потоках могут произвольно чередоваться средой выполнения, два потока могут одновременно считывать значение, оба видят одно и то же значение, а затем оба добавляют к нему одно. В результате один и тот же порядковый номер возвращается из нескольких вызовов в разных потоках.
Мы могли бы написать функцию get-next, используя атом Clojure, и такое же условие гонки не было бы проблемой.
user=> (def uniq-id (atom 0)) #'user/uniq-id user=> (defn get-next [] (swap! uniq-id inc)) #'user/get-next user=> (get-next) 1 user=> (get-next) 2
Приведенный выше код демонстрирует результат вызова get-next несколько раз (
функция inc просто добавляет единицу к переданному значению). Так как мы не находимся в многопоточной среде, пример не захватывает дух; однако то, что на самом деле происходит под обложками, очень хорошо описано на clojure.org/atoms —
[Y] измените значение, применив функцию к старому значению. Это делается атомным способом с помощью свопа! Внутренне поменяйся! читает текущее значение, применяет к нему функцию и пытается сравнить и установить его. Поскольку другой поток мог изменить значение за прошедшее время, ему, возможно, придется повторить попытку, и это происходит в цикле вращения. Чистый эффект состоит в том, что значение всегда будет результатом применения предоставленной функции к текущему значению атомарно.
Также помните, что изменения в атомах происходят синхронно, поэтому наша функция get-next никогда не будет возвращать одно и то же значение дважды.
(примечание: хотя Java уже предоставляет класс AtomicInteger для решения этой проблемы — дело не в этом. Смысл примера в том, чтобы показать, что Atom безопасен для использования в потоках.)
Если вы действительно заинтересованы в проверке того, что атом «Радость Clojure» безопасна для всех потоков,
предоставляет следующий фрагмент кода (а также прекрасное объяснение всех вещей Clojure, включая изменчивость).
(import '(java.util.concurrent Executors)) (def *pool* (Executors/newFixedThreadPool (+ 2 (.availableProcessors (Runtime/getRuntime))))) (defn dothreads [f & {thread-count :threads exec-count :times :or {thread-count 1 exec-count 1}}] (dotimes [t thread-count] (.submit *pool* #(dotimes [_ exec-count] (f))))) (def ticks (atom 0)) (defn tick [] (swap! ticks inc)) (dothreads tick :threads 1000 :times 100) @ticks ;=> 100000
Там у вас это есть, 1000 обновленных тем отмечены 100 раз без проблем.
Атомы прекрасно работают, когда вы хотите застраховать атомарные обновления для отдельного элемента состояния; однако, вероятно, пройдет немного времени, прежде чем вы захотите скоординировать какое-либо обновление состояния. Например, если вы работаете в интернет-магазине, когда клиент отменяет заказ, он либо активен, либо отменен; однако заказ никогда не должен быть активным и отменен. Если бы вы сохраняли набор активных заказов и набор отмененных заказов, вы бы никогда не захотели, чтобы заказ был в обоих наборах одновременно. Clojure решает эту проблему с помощью ссылок. Ссылки похожи на атомы, но они также участвуют в согласованных обновлениях.
В следующем примере показана функция отмены заказа, которая перемещает идентификатор заказа из набора активных заказов в набор отмененных заказов.
user=> (def active-orders (ref #{2 3 4})) #'user/active-orders user=> (def cancelled-orders (ref #{1})) #'user/cancelled-orders user=> (defn cancel-order [id] (dosync (<a href="http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/commute">commute</a> active-orders <a href="http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/disj">disj</a> id) (commute cancelled-orders <a href="http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/conj">conj</a> id))) #'user/cancel-order user=> (cancel-order 2) #{1 2} user=> @active-orders #{3 4} user=> @cancelled-orders #{1 2}
Как видно из примера, мы перемещаем идентификатор заказа с активного на отмененный. Опять же, наша
сессия REPL не показывает силу того, что происходит с реф, но clojure.org/refs содержит хорошее объяснение —
Все изменения, внесенные в Refs во время транзакции (через ref-set,
alter или commute), будут появляться в одной точке временной шкалы Ref world (ее «точка записи»).
Приведенная выше цитата — это всего лишь 1 элемент в списке из 10 пунктов, в котором обсуждается, что на самом деле происходит. Стоит просмотреть список несколько раз, пока вы не почувствуете себя комфортно со всем, что происходит. Но вам не нужно полностью понимать все, чтобы начать. Вы можете начать экспериментировать с ссылками в любое время, когда вам понадобятся скоординированные изменения в нескольких частях штата.
Когда вы впервые начинаете смотреть на ссылки, вы можете задаться вопросом, стоит ли вам использовать commute или alter. В большинстве случаев коммутирование обеспечит больше параллелизма и является предпочтительным; однако вам может потребоваться гарантировать, что ссылка не была обновлена в течение срока действия текущей транзакции. Обычно это тот случай, когда в игру вступает alter. В следующем примере показано использование commute для обновления двух значений. Пример демонстрирует, что пары всегда обновляются только один раз; однако это также показывает, что функция просто применяется к текущему значению, поэтому приращение не является последовательным, и @uid может быть разыменовано до одного и того же значения несколько раз.
user=> (def uid (ref 0)) #'user/uid user=> (def used-id (ref [])) #'user/used-id user=> (defn use-id [] (dosync (commute uid inc) (commute used-id conj @uid))) #'user/use-id user=> (dothreads use-id :threads 10 :times 10) nil user=> @used-id [1 2 3 4 5 6 7 8 9 10 ... 89 92 92 94 93 94 97 97 99 100]
Приведенный выше пример показывает, что коммутирование просто применяется независимо от базового значения. В результате вы можете увидеть повторяющиеся значения и пробелы в вашей последовательности (показано в 90-х годах в нашем выводе). Если вы хотите убедиться, что значение не изменилось во время транзакции, вы можете переключиться на изменение. В следующем примере показано поведение перехода от коммутирующих к изменениям.
user=> (def uid (ref 0)) #'user/uid user=> (def used-id (ref [])) #'user/used-id user=> (defn use-id [] (dosync (alter uid inc) (alter used-id conj @uid))) #'user/use-id user=> (dothreads use-id :threads 10 :times 10) nil user=> @used-id [1 2 3 4 5 6 7 8 9 10 ... 91 92 93 94 95 96 97 98 99 100]
Есть более продвинутые примеры использования ссылок в «Радости Clojure» для тех из вас, кто хочет обсудить условия углового случая.
Наконец, что не менее важно, агенты также доступны. С clojure.org/agents —
Как и Refs, агенты предоставляют общий доступ к изменяемому состоянию. Если Refs поддерживают скоординированное синхронное изменение нескольких местоположений, агенты обеспечивают независимое асинхронное изменение отдельных местоположений.
Хотя я концептуально понимаю агентов, я практически не использовал их на практике. Некоторые люди
любят их , и моя последняя команда переключилась на интенсивное использование агентов в одном из наших приложений вскоре после моего ухода. Но у меня лично нет достаточного опыта, чтобы точно сказать, где, по моему мнению, они вписываются. Я уверен, что это будет темой для будущего сообщения в блоге.
Надеюсь, что между эссе Рича и приведенными выше примерами стало ясно несколько вещей:
- Clojure имеет много поддержки для управления государством
- Различие Рича между идентичностью и ценностью позволяет Clojure извлекать выгоду из неизменных структур, а также позволяет переназначать идентичность.
- Clojure, это о состоянии.
С http://blog.jayfields.com/2011/04/clojure-state-management.html