Clojure Dojo
В этот понедельник я ходил на кложурное доджо в Лондоне, организованное компанией uSwitch. Доджо это мероприятия, проводимые каждые 2 недели какой-либо компанией, в основном это ThoughtWorks и uSwitch. Собираются около 10-15 человек, заказыват пиццу, предлагают идеи для кодинга, потом делятся на команды по 3-5 человек в каждой и кодят какую-либо идею в течение часов 2. После кодинга каждая команда делает короткую 5-минутную презентацию о том, что же они наделали за вечер. Главное правило в доджо (кроме "получать удовольствие") - каждый участник должен хоть немного покодить, написать хоть пару строчек, но должен.
Народ на доджо собирается разношёрстный. Некоторые годами кодят на кложуре и используют его на работе, другие же на прошлой неделе начали изучать и написали только "Hello, world". Идеи для проектов обычно не очень сложные, в конце концов даётся только 2 часа, и у многих участников опыта в кложуре немного. Например, на последнем доджо организовалось 5 команд. Две из них были командами новичков, они работали над упражнениями из exercism.io. Две другие команды пробовали на вкус Quil на ClojureScript, поддержку которого, кстати, добавил Максим Карандашов, Google Summer of Code студент из Саратова. И наконец пятая команда (в которой я был) работала над PubSub библиотекой работающей на уровне локальной сети.
Один из ключевых аспектов в доджо - среда разработки. Команда обычно работает на одном ноутбуке, потому что шарить код в реальном времени между несколькими ноутбуками не очень просто. Так что получается, что кто-то один кодит, а остальные обсуждают задачу и помогают ему. Один из уроков, который я усвоил после посещения пары-тройки доджо, это то, что редактор должен быть как можно проще. Конечно у каждого есть собственный любимый emacs/vim/что-нибудь ещё с настроенными примочками, но использовать их в доджо будет плохой идеей. Я был на нескольких доджо, на которых мы большую часть времени потратили сражаясь с специфичными кей-биндингами в емаксе вместо того, чтобы писать кложур. Это расстраивает. А если представить, что вы новичок и пришли на доджо, то вам мало того, что надо писать на неведомом языке, так ещё и в непонятном, нелогичном редакторе. Это расстраивает вдвойне. Так что редактор должен быть простым. Я считаю, что LightTable - лучший вариант для кложурных доджо. Он легко запускается, позволяет писать текст без выкручивания пальцев и мозга, и наконец, позволяет легко исполнять кложурный код, просто нажмите "Ctrl+Enter".
Теперь я хотел бы описать проект, на которым наша команда работала на доджо: PubSub
PubSub
Идея достаточно проста: написать библиотеку, состоящую из 2 функций: publish
и subscribe
. Она должна удовлетворять следующим требованиям:
- Посылка сообщений и подписка должны происходить на уровне сети, т.е. каждый компьютер должен получать сообщения опубликованные другими компьютерами в этой же сети.
publish
принимает 1 аргумент - сообщение. Сообщение - это произвольный кложурный объект (строка, мапа, вектор или что-нибудь ещё).subscribe
принимает 1 аргумент - функцию-обработчик, которая будет обрабатывать полученные сообщения.
Как можно видеть, у библиотеки не должно быть концепции адресов, очередей или топиков. Также не должно быть концепции сервера: клиенты должны быть способны общаться друг с другом без надобности подсоединения к какому-либо серверу.
Для реализации мы решили воспользоваться IP мультикастом. Мы не лезли глубоко в дебри спецификации и технических деталей, просто нагуглили, как это сделать в джаве. Нашли java.net.MulticastSocket
класс, который делает как раз то, что нам и надо. Более того, джавадок для этого класса содержит пример, как его использовать. Фактически, мы просто переписали пример на кложур и немного расширили. Собственно код:
(ns pubsub.core
(:import [java.net InetAddress DatagramPacket MulticastSocket]))
; константы определяющие адрес мультикаста
(def address "228.5.6.7")
(def port 6789)
(def group (InetAddress/getByName address))
; функция для создания мультикаст-сокета
(defn init-comm []
(let [s (MulticastSocket. port)]
(.joinGroup s group)
s))
; определяем глобальный сокет, которые используется для посылки сообщений
(def socket (init-comm))
(defn get-packet [message group port]
(DatagramPacket. (.getBytes message) (.length message) group port))
; определяем 'publish' функцию. По каким-то причинам мы назвали её 'send-it'
(defn send-it [message]
(.send socket (get-packet (pr-str message) group port)))
Теперь мы умеем посылать сообщения, круто! Как можно видеть, мы используем EDN в качестве формата сообщения (pr-str
конвертирует объект в EDN строку). Теперь настало время реализовать механизм подписки. Мы немного отклонились от начальных условий и реализовали чуть более сложную модель. Добавлены 2 фичи, которые изначально не планировались:
- Вместо того, чтобы передавать только функцию-обработчик, мы можем передавать 2 функции: предикат и функцию-обработчик. Предикат проверяет, удовлетворяет ли сообщение какому-либо критерию и если удовлетворяет, то вызывает функцию-обработчик.
- Изначально механизм прекращения подписки не предусматривался. Мы его добавили. Когда вы подписываетесь создаётся promise объект. Он используется как флаг конца подписки: как только кто-то положит что-либо в него, то подписка будет остановлена. Этот объект возвращается из функции
subscribe
, так что пользователь может положить что-нибудь в него, когда нужно остановить подписку.
И теперь код:
; 'process-message' - рекурсивная функция для обработки 1 сообщения
; Аргументы
; socket - сокет, из которого мы будем считывать сообщение
; predicate - предикат, заданный пользователем
; handler - функция-обработчик, заданная пользователем
; finished - объект promise, для прекращения подписки
(defn process-message [socket predicate handler finished]
(let [size 1000
packet (DatagramPacket. (byte-array size) size)]
(when-not (realized? finished)
(.receive socket packet)
(let [obj (-> packet .getData (String.) read-string)]
(when (predicate obj)
(handler obj))))
(recur socket predicate handler finished)))
; функция 'subscribe'. Мы снова назвали её по-другому :)
(defn subscribe-with
([handler]
(subscribe-with (constantly true) handler))
([predicate handler]
(let [socket (init-comm)
finished (promise)]
(future
(process-message socket predicate handler finished)
(.leaveGroup socket group))
finished)))
Вот и всё. Библиотека готова к использованию. Далее пример использования:
(require '[pubsub.core :refer [send-it subscribe-with]])
; подписаться на все сообщения
(subscribe-with #(println "Simplest subscribe" %))
; послать сообщение, оно должно отобразиться в консоли
(send-it "something")
; подписаться только на сообщения-мапы, в которых топик == :dojo
(subscribe-with #(= (:topic %) :dojo)
#(println "Dojo message" %))
; подписаться только на сообщения с топиком :work
(subscribe-with #(= (:topic %) :work)
#(println "Work message" %))
; подписаться только на сообщения с топиком :home
(subscribe-with #(= (:topic %) :home)
#(println "Home message" %))
; посылаем сообщения с разными топиками
(send-it {:topic :dojo :message "Hello, clojurians!"})
(send-it {:topic :work :message "You're at work..."})
(send-it {:topic :home :message "You're at home..."})
Далее можно написать что-нибудь на основе этой библиотеки, например децентрализованный чат или простую мультиплеерную игру. Может это будет интересным проектом для какого-нибудь другого доджо.
Если интересно поиграться с кодом - он доступен на GitHub.