Оставшиеся ссылочные типы

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

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

(let [a (atom 1000)
      b (atom 0)]
  (future (swap! a - 100)
          (Thread/sleep 100)
          (swap! b + 100))
  (Thread/sleep 50)
  {:total (+ @a @b)
   :a     @a
   :b     @b}) ;;=> {:total 900, :a 900, :b 0}

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

Существуют и другие ссылочные типы, которые могут быть синхронными/асинхронными и скоординированными/нескоординированными.

Ref

С их помощью реализуется механизм Software Transaction Memory (STM).

(let [a (ref 1000)
      b (ref 0)]
  (future (dosync
           (alter a - 100)
           (Thread/sleep 100)
           (alter b + 100)))
  (Thread/sleep 50)
  {:total (+ @a @b)
   :a     @a
   :b     @b}) ;;=> {:total 1000, :a 1000, :b 0}

(let [a (ref 1000)
      b (ref 0)]
  (future (dosync
           (alter a - 100)
           (Thread/sleep 100)
           (alter b + 100)))
  (Thread/sleep 150)
  {:total (+ @a @b)
   :a     @a
   :b     @b}) ;;=> {:total 1000, :a 900, :b 100}

dosync - начинает транзакцию, alter - аналог swap!, только для ref, обязательно вызывается внутри dosync.

В примере с ref в любой момент времени состояние счетов непротиворечиво, и ссылки обновляются согласовано.

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

Для изменения значения ссылки есть следующие функции:

Т.е. это скоординированный ссылочный тип, с синхронными операциями.

Var

Мы используем их с начала знакомства с clojure.

Когда мы вызываем (def x 1) или (defn f [arg] 1), то на самом деле создаем var(переменную).

Вместо того, чтобы постоянно писать (@f arg) для вызова функции, хранящейся в переменной f, ввели упрощенный синтаксис: (f arg).

Чтобы получить саму переменную - нужно воспользоваться специальной формой (var f) или #'f. Наверняка, вы видели #'user/f, когда выполняли выражение (defn f []). user - это пространство имен, в котором определена переменная.

Таким образом (f arg) эквивалентно (@#'f arg).

Переменные позволяют переопределять свое значение для всех потоков:

(defn f [] 1)

(f) ;;=> 1

(alter-var-root #'f
                (fn [old-value]
                  (fn [] 2)))

(f) ;;=> 2

alter-var-root можно использовать для декорирования функций:

(defn f [] (prn :ok))
(alter-var-root #'f memoize)
(f) => вернет nil и напечатает :ok
(f) => просто вернет nil

memoize - стандартная функция, принимающая функцию, и возвращающая мемоизированный вариант.

Повторюсь, что x - получение значения переменной, а не самой переменной:

(def x 1)

(let [x' x] ;; запомнили содержимое x в x'
  (alter-var-root #'x inc)
  [x' x]) ;;=> [1 2]

Если мы используем функции, то значение извлекается на каждый вызов:

(def x 1)

(defn f [] x)

(f) ;;=> 1

(alter-var-root #'x inc)

(f) ;;=> 2

Если мы хотим ссылаться на саму переменную, то нужно поступить так:

(def x 1)

(let [x' #'x] ;; запомнили саму переменную x в x'
  (alter-var-root #'x inc)
  [@x' x]) ;;=> [2 2]

Т.е. переменные, как и прочие ссылочные типы, позволяют получить свое значение с помощью @.

Переменные реализуют интерфейс функций:

(defn f [] 1)

;; вызываем функцию
(f) ;;=> 1

;; получаем переменную, извлекаем ее значение и вызываем это значение
;; аналогично предыдущему
(@#'f) ;;=> 1

;; используем переменную в качестве функции
(#'f) ;;=> 1

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

(defn f [] 1)

(defn inspect [f]
  (fn [& args]
    (prn args) ;; печатаем аргументы
    (let [result (apply f args)] ;; вызываем функцию с аргументами в виде коллекции
      (prn result) ;; печатаем результат
      result)))

(let [f1 (inspect f) ;; передача по значению
      f2 (inspect #'f)] ;; похоже на передачу по ссылке
  (alter-var-root #'f (fn [old]
                        (fn [] 2)))
  [(f1) (f2)]) ;;=> [1 2]

Переменную можно переопределить для определенной области и вернуть исходное значение:

(defn f [] 1)

(with-redefs [f (fn [] 2)]
  (f)) ;;=> 2

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

Этот способ не работает для inline функций, т.к. они не используют var, т.е. для большинства стандартных:

(with-redefs [+ -]
  (+ 2 1)) ;;=> 3

Все переменные по умолчанию статические. Бывают еще и динамические. Они позволяют переопределить свое значение только для текущего потока. По соглашению таким переменным надевают наушники: *some-var*.

(def ^:dynamic *x* 1)

(let [a (future
          (binding [*x* 2]
            (Thread/sleep 100)
            *x*))
      b (future
          (Thread/sleep 50)
          *x*)]
  [@a @b]) ;;=> [2 1]

Если бы binding переопределял значение для всех потоков, то @b вернул бы 2.

При этом, clojure функции, умеют запоминать контекст, а java tread - нет:

(def ^:dynamic *x* 1)

(binding [*x* 2]
  (future (prn "future" *x*))
  (.start (Thread. (fn [] (prn "thread" *x*))))

;; вывод на печать:
;; "future" 2
;; "thread" 1

Ленивые коллекции, future и т.п. умеют запоминать контекст треда начиная с версии clojure 1.3. Сторонние библиотеки, вроде core.async, также сохраняют контекст. Есть макрос bound-fn с помощью которого вы можете запомнить контекст, например, при работе с java interop.

В дальнейшем мы будем использовать динамические переменные для внедрения зависимостей. Внедрять зависимость можно и с помощью alter-var-root, но как быть, если вам нужен новый инстанс зависимости на каждый запрос, например сессия пользователя. Или вы захотите запуситиь несколько экземпляров приложения с разными зависимостями в одном JVM процессе.

Переменные могут быть и локальными, смотри with-local-vars.

Так же переменные можно сделать приватными, т.е. они станут доступны только в пределах своего неймспейса:

(def ^:private x 1)
(defn ^:private f [])
(defn- g [])

По нашей классификации это нескоординированный ссылочный тип, с синхронными операциями.

Agent

Агент - контейнер для значения с очередью операций над ним:

(let [a (agent 0)]
  (send a (fn [old]
            (Thread/sleep 50)
            (inc old)))
  @a) ;;=> 0

(let [a (agent 0)]
  (send a inc)
  (await a) ;; ждем, пока обработается очередь
  @a) ;;=> 1

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

При этом агенты встроены в STM, т.е. сообщение будет отправлено только после успешного завершения транзакции:

(let [counter     (ref 0)
      calls-atom  (atom 0)
      calls-agent (agent 0)]
  (->> (repeatedly #(future
                      (dosync
                       (swap! calls-atom inc)
                       (send calls-agent inc)
                       (alter counter inc))))
       (take 100)
       (doall)
       (map deref)
       (doall))
  (await calls-agent) ;; ждем, пока обработается очередь
  {:counter     @counter
   :calls-atom  @calls-atom
   :calls-agent @calls-agent}) ;;=> {:counter 100, :calls-atom 102, :calls-agent 100}

Атомы не интегрированы в STM, поэтому при повторении транзакции из-за конфликтов calls-atom показывает количество успешных и неуспешных транзакций. Но агент интегрирован в STM и получает сообщения только после успешного завершения транзакции.

Функция send использует системный тредпул, и если функция может долго выполняться, то используют send-off, который выполняет эту функцию вне системного тредпула.

Это асинхронный несогласованный ссылочный тип.

Валидаторы и наблюдатели

Все ссылочные типы позволяют установить валидатор и добавить наблюдателей:

Volatile

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

(let [v (volatile! 0)]
  (vswap! v inc)
  @v) ;;=> 1

Он реализован как тривиальный java класс, хранящий состояние в volatile переменной. JVM содержит оптимизации, и если один поток изменил переменную, другие потоки могут не увидеть это изменение. Для этого случая в java есть ключевое слово volatile, которое показывает, что значение переменной может быть изменено в другом потоке.

Естественно, он не гарантирует атомарности как атом:

(let [counter-a (atom 0)
      counter-v (volatile! 0)]
  (->> (repeatedly #(future
                      (swap! counter-a inc)
                      (vswap! counter-v inc)))
       (take 100)
       (doall)
       (map deref)
       (doall))
  {:counter-a  @counter-a
   :counter-v  @counter-v}) ;;=> {:counter-a 100, :counter-v 98}