Участвовали в Clojure Cup 2013 вместе с Саней ingspree, Сергеем Joes и Ромой rofh. Наверное, вы видели нарезку, а может и полное выступление Сани о кложурскрипте и реактивном программировании. Вот и подвернулась возможность попробовать эти технологии в бою.
Тематика соревнования — за 48 часов напилить что-нибудь, используя Clojure или ClojureScript. Из разных вариантов решено было пилить браузерный Risk, в частности потому, что все существующие приложения убоги интерфейсом, или написаны на Flash, или, ещё того хуже — Silverlight, или ещё каким-нибудь образом портят времяпровождение. Коллективно придумалось хорошее название — War Magnet.
Недолго думая, мы решили писать и сервер на Clojure, и клиент на ClojureScript, с использованием общего кода. Из четырёх участников только у Сани был хоть какой-то опыт написания на кложуре, а у остальных был только опыт решения нескольких десятков задачек на 4clojure.
Сервер
По серверной части я много не расскажу — за всё время соревнования я так ни разу к серверной части и не притронулся. В качестве базы данных мы выбирали из двух вариантов:
Первое, что приходит в голову при сочетании слов Clojure и «база данных» — Datomic. Очень интересно попробовать, но ни у кого из нас не было абсолютно никакого опыта с датомиком, даже пары часов, и мы опасались, что ещё одна совершенно новая концепция в проекте может загубить начинание и мы ничего не успеем.
Поэтому выбор пал на самую модную в последние годы базу данных — PostgreSQL.
Пытались использовать clony от si14 в качестве заготовки, но для нас оказалось слишком сложно, очень непонятно было, как расширять своими вещами. Через полдня после начала сделали новый репозиторий без clony, и быстро перенесли все наработки туда.
Перечислю библиотеки, которые мы использовали на сервере — compojure, ring, korma, friend, cheshire, http-kit. Краем уха слышал о корме нелицеприятные комментарии, а все остальные подробности по этому поводу Сергей обещал описать у себя в блоге.
Клиент
Выбирали, какие технологии использовать на клиенте. Есть новомодный core.async, но он отпал по причине того, что все туториалы и руководства пишут о том, как классно управлять данными, а потом прибиваются к DOM'у селекторами. Есть мнение, что это ущербная концепция, и надо просто принять, что HTML — это то, как мы строим интерфейсы. А если селекторами присоединяться — то мы как бы сбоку от него работаем, а из-за любого изменения структуры интерфейса приходится очень аккуратно и нудно перепиливать эти чёртовы селекторы.
Есть хорошо выглядящие реактивные библиотеки для ClojureScript — связка из javelin и hoplon. Они хоть и хороши с концептуальной точки зрения, но там никто пока оптимизациями не занимался и потому уж очень они медленные — простейший Todo-пример заметно тормозит даже на десктопном файрфоксе. Разработка более сложного приложения превратилась бы в боль из-за постоянных тормозов интерфейса, решили отказать.
В последнем проекте по работе Саня и Рома используют фейсбуковый React в связке с кофескриптом, и он им очень нравится. Вот его и взяли. В пятницу, перед началом соревнования, я начал понемногу разбираться с React'ом и начал писать библиотеку-обвязку для него. Доделали первую версию мы уже в субботу к часу дня.
Вот так выглядит React:
var HelloMessage = React.createClass({
displayName: 'HelloMessage',
componentWillMount: function() {
...
},
render: function() {
return <div class="smth">{'Hello ' + this.props.name}</div>;
}
});
Этот XML-подобный синтаксис удобен и понятен в джаваскрипте. Но интегрировать его в Clojure даже мысли не возникло: вдохновившись синтаксисом hiccup, мы смастерили вот такой вариант:
(defr HelloMessage
:component-will-mount (fn [] ...)
[this props state]
[:div.smth (str "Hello " (:name props))])
Некоторые проблемы возникли на стыке кложурскрипта и реакта. Например, мы сначала попробовали напрямую использовать ClojureScript'овые структуры данных в качестве состояния, но реакт у себя внутри делает shallow копию, не перенося прототип, и у нас всё ломалось. В качестве костыля начали складывать наше состояние в поле state.state
, и доставать его наружу.
Конечно, оно спрятано в библиотеке, но из-за этого пришлось делать хелперы assoc-state
и assoc-in-state
, которые нужно использовать для изменения состояния. Один из разработчиков Реакта — Pete Hunt — в irc предложил такой же обходной путь. Может, как-нибудь удастся их подружить более адекватным способом.
В целом, на клиенте мы всё состояние храним в одном атоме, в одной структуре данных, которую по нужным частям передаём в контроллеры глубже и глубже. С джаваскриптом у нас бы случился локальный апокалипсис, но в кложуре структуры данных немутабельны, поэтому никаких проблем не возникло.
Для общения между сервером и клиентом мы использовали json, потому как крутой кложурный формат edn на клиенте десериализуется медленнее приблизительно на порядок, а cljson, который сохраняет кложурные структуры, показался смешным и непонятным. И вот это оказалось ошибкой!
Потом полезли проблемы с тем, что :keyword
сериализуется как «keyword». Кложуре можно сказать :keywordize-keys
— сделай в словарях из ключей кейворды. Но это всех проблем не решает — не все кейворды были ключами в словарях, и создаёт другие — не все ключи в словарях были кейвордами. Особенно неприятно оказалось с числами — серверная Clojure вообще не может сделать (keyword 1)
и возвращает nil
, а ClojureScript сделает :1
, но потом окажется, что десериализованные со спецопцией ключи из json'а содержат внутри строку, а не число, т.е (keyword "1")
.
Со второй половины воскресенья мы потеряли минимум полтора часа на этой проблеме, и сейчас по коду там и сям расставлены костыли. Нужно было изначально использовать cljson, и, наверное, переделаем на его использование.
Вот так выглядит код для этого окна:
(defr Attack
[C {:keys [attacker attacking defender defending attack!]} S]
(let [[aname {:keys [coordinates]}] attacker
[dname dmap] defender
[x y] (xy-for-popover coordinates)]
[:div.popover {:style (clj->js {:display "block" :left x :top y})}
[:div.popover-content
[:table
[:thead [:tr [:th (name aname)] [:th (name dname)]]]
[:tbody [:tr [:td attacking] [:td defending]]]]
[:div.btn-group
[:button.btn.btn-warning {:on-click #(attack! 1 aname dname)} "Attack"]
[:button.btn.btn-danger {:on-click #(attack! (dec attacking) aname dname)} "Blitz"]]]]))
По-моему, всё происходящее здесь довольно понятно. А если кого-то напрягает здесь количество скобочек, так вспомните, что в настоящем HTML их ещё в два раза больше: <div></div>
— четыре, [:div]
— две. Плюс, при редактировании очень помогает paredit — с ним в скобках не запутываешься вообще.
В целом, сложилось ощущение, что ClojureScript и React — уматовая связка, использовать можно и нужно!
Соревнование
Клич по поводу участия в Clojure Cup Саня кинул за две недели до, тогда же договорились что будем делать, и приблизительно какие технологии использовать. Но, как водится, к таких соревнованиям почти никто не готовится, несмотря на любые обещания себе и товарищам, и мы не исключение. Хоть библиотеку мы начали делать в пятницу вечером (это разрешено правилами)!
Сам Clojurecup длился ровно 48 часов выходных, с 00:00 UTC субботы до 00:00 UTC понедельника. По нашему времени это три часа ночи.
Собравшись с утра субботы в офисе, мы потратили где-то с полдня на доделывание библиотеки, всякие сетапы и прочую раскачку до рабочего состояния.
За субботу у нас появилась авторизация через мозилловскую Персону (классная штука!), посылка сообщений между клиентом и сервером через вебсокеты, немножечко общего кода между клиентом и сервером, в базе данных — таблички с пользователями, играми и логом событий. Ещё на клиенте началась рисоваться классическая Risk-карта с территориями и как-то подсвечиваться при наведении. Последний коммит в 22:30.
С утра воскресенья я нарисовал нам симпатичный логотип, а потом у меня всё воскресенье смазано в одно непрерывное педаленье кода. Связывание игровой карты и сервера, ходы-атаки-пополнения и прочее фактически осталось на конец.
К вечеру оно всё ещё было в разобранном состоянии, к восьми часам мы немного переделали формат описания карты и первый раз её загрузили на сервере. Так как ещё было совершенно непонятно, успеваем ли мы доделать игру до минимально рабочего состояния, мы решили продолжать, пока будет виден шанс и желание/возможность что-то делать.
Где-то часов в восемь-девять мы переместились в другую комнату, где было намного лучше освещение, прохладно и ближе до уголка с чаем и кофе :) Получилось так, что всё время был виден шанс сделать рабочий вариант, энергии и задора хватало, и мы пилили-пилили его аж вплоть до дедлайна.
Коммит с рабочей игрой и кнопкой «окончить ход»:
Mon Sep 30 2013 02:59:54 GMT+0300 (EEST)
К сожалению, у нас в продакшен версию закрался баг с загрузкой данных карты из файла. Локально оно работает, а при упаковке в uberjar — нет, его нужно грузить из ресурсов. Коммит с исправлением этого был за 5 минут до финиша, но он оказался неудачный, и у нас выложена версия, в которую поиграть нельзя. Не хватило буквально пятнадцати минут.
По правилам, мы не имеем права ничего доисправлять или выкладывать другую версию где-нибудь. Сейчас идёт голосование, оно закончится в четверг и в пятницу можно будет обновить и показать полностью работающее.
Всего было зарегистрировано больше 90 команд. У нас получилось 6.5% коммитов от общего количества коммитов всех команд, причём есть минимум одна команда, у которой получилось ещё больше, кажется 9.15%. Для голосования отобрано 42 команды. У меня почему-то в файрфоксе их сайт толком не работает. В хроме работает.
Я обещаю, что в пятницу мы в любом случае выложим рабочую версию, а пока что на страничке нашей команды можно за нас проголосовать!
Автор: Murkt