- PVSM.RU - https://www.pvsm.ru -

Битва за Кложуру или операция «Боевой Магнит»

Участвовали в Clojure Cup 2013 вместе с Саней ingspree [1], Сергеем Joes [2] и Ромой rofh [3]. Наверное, вы видели нарезку [4], а может и полное выступление [5] Сани о кложурскрипте и реактивном программировании. Вот и подвернулась возможность попробовать эти технологии в бою.

Тематика соревнования — за 48 часов напилить что-нибудь, используя Clojure или ClojureScript. Из разных вариантов решено было пилить браузерный Risk [6], в частности потому, что все существующие приложения убоги интерфейсом, или написаны на Flash, или, ещё того хуже — Silverlight, или ещё каким-нибудь образом портят времяпровождение. Коллективно придумалось хорошее название — War Magnet [7].

Недолго думая, мы решили писать и сервер на Clojure, и клиент на ClojureScript, с использованием общего кода. Из четырёх участников только у Сани был хоть какой-то опыт написания на кложуре, а у остальных был только опыт решения нескольких десятков задачек на 4clojure [8].

Сервер

По серверной части я много не расскажу — за всё время соревнования я так ни разу к серверной части и не притронулся. В качестве базы данных мы выбирали из двух вариантов:

Первое, что приходит в голову при сочетании слов Clojure и «база данных» — Datomic [9]. Очень интересно попробовать, но ни у кого из нас не было абсолютно никакого опыта с датомиком, даже пары часов, и мы опасались, что ещё одна совершенно новая концепция в проекте может загубить начинание и мы ничего не успеем.

Поэтому выбор пал на самую модную в последние годы базу данных — PostgreSQL.

Пытались использовать clony [10] от si14 [11] в качестве заготовки, но для нас оказалось слишком сложно, очень непонятно было, как расширять своими вещами. Через полдня после начала сделали новый репозиторий без clony, и быстро перенесли все наработки туда.

Перечислю библиотеки, которые мы использовали на сервере — compojure, ring, korma, friend, cheshire, http-kit. Краем уха слышал о корме нелицеприятные комментарии, а все остальные подробности по этому поводу Сергей обещал описать у себя в блоге [12].

Клиент

Выбирали, какие технологии использовать на клиенте. Есть новомодный core.async [13], но он отпал по причине того, что все туториалы [14] и руководства пишут о том, как классно управлять данными, а потом прибиваются к DOM'у селекторами [15]. Есть мнение, что это ущербная концепция, и надо просто принять, что HTML — это то, как мы строим интерфейсы. А если селекторами присоединяться — то мы как бы сбоку от него работаем, а из-за любого изменения структуры интерфейса приходится очень аккуратно и нудно перепиливать эти чёртовы селекторы.

Есть хорошо выглядящие реактивные библиотеки для ClojureScript — связка из javelin [16] и hoplon [17]. Они хоть и хороши с концептуальной точки зрения, но там никто пока оптимизациями не занимался и потому уж очень они медленные — простейший Todo-пример [18] заметно тормозит даже на десктопном файрфоксе. Разработка более сложного приложения превратилась бы в боль из-за постоянных тормозов интерфейса, решили отказать.

В последнем проекте по работе Саня и Рома используют фейсбуковый React [19] в связке с кофескриптом, и он им очень нравится. Вот его и взяли. В пятницу, перед началом соревнования, я начал понемногу разбираться с React'ом и начал писать библиотеку-обвязку [20] для него. Доделали первую версию мы уже в субботу к часу дня.

Вот так выглядит React:

var HelloMessage = React.createClass({
  displayName: 'HelloMessage',
  componentWillMount: function() {
    ...
  },
  render: function() {
    return <div class="smth">{'Hello ' + this.props.name}</div>;
  }
});

Этот XML-подобный синтаксис удобен и понятен в джаваскрипте. Но интегрировать его в Clojure даже мысли не возникло: вдохновившись синтаксисом hiccup [21], мы смастерили вот такой вариант:

(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 [22] на клиенте десериализуется медленнее приблизительно на порядок, а cljson [23], который сохраняет кложурные структуры, показался смешным и непонятным. И вот это оказалось ошибкой!

Потом полезли проблемы с тем, что :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 понедельника. По нашему времени это три часа ночи.

Собравшись с утра субботы в офисе, мы потратили где-то с полдня на доделывание библиотеки, всякие сетапы и прочую раскачку до рабочего состояния.

За субботу у нас появилась авторизация через мозилловскую Персону [24] (классная штука!), посылка сообщений между клиентом и сервером через вебсокеты, немножечко общего кода между клиентом и сервером, в базе данных — таблички с пользователями, играми и логом событий. Ещё на клиенте началась рисоваться классическая Risk-карта с территориями и как-то подсвечиваться при наведении. Последний коммит в 22:30.

С утра воскресенья я нарисовал нам симпатичный логотип, а потом у меня всё воскресенье смазано в одно непрерывное педаленье кода. Связывание игровой карты и сервера, ходы-атаки-пополнения и прочее фактически осталось на конец.

Битва за Кложуру или операция «Боевой Магнит»

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

Где-то часов в восемь-девять мы переместились в другую комнату, где было намного лучше освещение, прохладно и ближе до уголка с чаем и кофе :) Получилось так, что всё время был виден шанс сделать рабочий вариант, энергии и задора хватало, и мы пилили-пилили его аж вплоть до дедлайна.

Коммит с рабочей игрой и кнопкой «окончить ход»:

Mon Sep 30 2013 02:59:54 GMT+0300 (EEST)

Битва за Кложуру или операция «Боевой Магнит»

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

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

Всего было зарегистрировано больше 90 команд. У нас получилось 6.5% коммитов от общего количества коммитов всех команд, причём есть минимум одна команда, у которой получилось ещё больше, кажется 9.15%. Для голосования отобрано 42 команды [25]. У меня почему-то в файрфоксе их сайт толком не работает. В хроме работает.

Я обещаю, что в пятницу мы в любом случае выложим рабочую версию, а пока что на страничке нашей команды можно за нас проголосовать [7]!

Автор: Murkt

Источник [26]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/44708

Ссылки в тексте:

[1] ingspree: http://habrahabr.ru/users/ingspree/

[2] Joes: http://habrahabr.ru/users/joes/

[3] rofh: https://twitter.com/rofh

[4] нарезку: http://habrahabr.ru/post/193950/

[5] полное выступление: http://www.youtube.com/watch?v=R4sTvHXkToQ

[6] Risk: http://en.wikipedia.org/wiki/Risk_%28game%29

[7] War Magnet: http://clojurecup.com/app.html?app=warmagnet

[8] 4clojure: http://www.4clojure.com/

[9] Datomic: http://datomic.com/

[10] clony: https://github.com/si14/clony/

[11] si14: http://habrahabr.ru/users/si14/

[12] блоге: http://mrjoes.github.io/

[13] core.async: https://github.com/clojure/core.async

[14] туториалы: http://rigsomelight.com/2013/07/18/clojurescript-core-async-todos.html

[15] селекторами: http://rigsomelight.com/2013/08/12/clojurescript-core-async-dots-game.html

[16] javelin: https://github.com/tailrecursion/javelin

[17] hoplon: https://github.com/tailrecursion/hoplon/

[18] Todo-пример: http://micha.github.io/todofrp/

[19] React: http://facebook.github.io/react/

[20] библиотеку-обвязку: https://github.com/piranha/pump

[21] hiccup: https://github.com/weavejester/hiccup

[22] edn: http://habrahabr.ru/post/178473/

[23] cljson: https://github.com/tailrecursion/cljson

[24] Персону: https://login.persona.org/about

[25] 42 команды: http://clojurecup.com/apps.html

[26] Источник: http://habrahabr.ru/post/195930/