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

    Участвовали в 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 {: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 команды. У меня почему-то в файрфоксе их сайт толком не работает. В хроме работает.

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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 19

      +3
      Классно, поздравляю с выступлением! :)
      Мы тоже участвовали в ClojureCup. У нас была распределённая команда: 1 белорус, 1 бразилец и 2 россиянина. Делали вебприложения для решения японских кроссвордов. В целом понравилось весьма, это был мой первый опыт участия в подобных мероприятиях. Опыта с вебразработкой, а особенно с клиентской частью (и clojurescript'ом) почти ни у кого не было, так что все прокачались немного по ходу конкурса.
      В конце концов получилось небольшое приложение с базовой функциональностью. Я доволен результатом. С деплоем решили не заморачиваться и на сервере просто запустили приложение через lein run, чтобы не мучаться с uberjar'ом, загрузкой ресурсов и другими сюрпризами.
      Страничка нашего проекта тут. Только следуя духу главного сайта clojurecup, который с файрфоксом не очень дружит, наше приложение тоже багованное в файрфоксе.
        +1
        Мы оба дня сидели все вместе в одной комнате, и нам это очень сильно помогло. Если бы сидели каждый отдельно, то сделали бы раза в полтора-два меньше, мне кажется.
          0
          Привет! Я даже зарешал один средний кроссворд в вашем приложение. Работает здорово.

          Мы не парились с автоматизацией вообще ничего — никаких хуков, деплой скриптов или еще чего. nohup lein trampoline run работает прекрасно.
          0
          Позволю себе рассказать и о нас.
          Мы делали CodeNotes — веб-проект для поиска TODO, FIXME и NOTE внутри репозитория на гитхабе с дополнительным функционалом в виде нотификаций для пользователей и markdown-форматированием для найденных туду.
          Плюс у нас можно сгенерить классный бейджик со счетчиком туду, который можно вставить в свой репозиторий на гитхабе.
          Варианты использования:
          1) Таск-трекер типа Jira для маленьких проектов — можно просто писать в коде TODO и FIXME, а потом в веб-интерфейсе легко это все смотреть.
          2) В README.md для своего репозитория можно больше не писать отдельную секцию TODO, а можно вставить наш бейджик.

          Но самое крутое это спектр используемых технологий. У нас есть веб-сокеты, вовсю используется angular.js, всякие дополнительные штуки типа миграций (чтобы на сервер все можно было переносить без особого труда), MariaDB для SQL, кэширование в памяти при помощи clojure core.cache. У нас сделан миллиард крутых серверных штук, которые в итоге делают так, что среднее время ответа сервера составляет 18ms при том, что сервера расположены в США. Обязательно напишем об этом пост чуть позже :)

          А вот здесь за нас можно проголосовать: clojurecup.com/app.html?app=codenotes
            0
            Я использовал Angular.js раньше, и ingspree тоже, но ни мне, ни ему не понравилось. Реакт намного лучше :)

            Про серверные штуки будет интересно почитать, обязательно пишите :) А в голосовании мы сейчас идём ноздря к ноздре.
              0
              Скажи, bleeding edge! У нас тоже веб-сокеты, тоже SQL (гг, опять в моде?), кеширование в памяти с помощью кложурных атомов и миграции моей личной библиотекой, гг.

              Надо сказать, что идея мне ваша нравится, но я пока не нашëл у себя репозиторий с TODO/FIXME, что мешает его потестировать. :\
                +1
                >кеширование в памяти с помощью кложурных атомов
                core.cache, тащемта, круче :) Там из коробки TTL-кеш и LRU. Для github API нужен TTL, для бейджей LRU.
                  +2
                  Канеша, но PgSQL круче мариидб и cljs на клиенте круче ангуляра! :) Так шо в сумме у нас 1 очко всë круче. :)))
                    0
                    Чёрт, да! :)
                    0
                    Кстати, спасибо за core.cache. Знал бы о нем раньше — использовал бы вместо одного из атомов.

                    Ну и буду писать пост о серверной архитектуре Warmagnet, пока свежо.

                    Если кратко, у нас прикольно получилось с синхронизацией состояния игры между клиентом и сервером. Есть общий код игровой логики, который принимает «мир» и мутирует его с помощью входящих действий. Любые действия игроков отправляются на сервер, тот их проверяет, изменяет локальное состояние и рассылает эти же действия всем клиентам смотрящим игру. Те уже применяют действие к своим локальным состояниям. Ну и поскольку на сервере хранится мастер копия, то реконнект клиента очень простой — тот просто получает готовое состояние игры сразу, ну а потом инкрементальные апдейты. Вся логика мутаций мира общая на клиенте и на сервере с помощью crossover (https://github.com/emezeske/lein-cljsbuild/blob/master/doc/CROSSOVERS.md)

                    По сути, получилось что-то типа node.js — возможность написания общего кода между клиентом и сервером, но clojure и в функциональном стиле.
                      0
                      Да, действительно. Если это чуть усугубить, получится полурешётка, CRDT и eventual consistency :)
                        0
                        Eventual им не надо, им strong надо, иначе придется состояния мира мержить. Вообще для таких штук надо что-то универсальное уже запилить, у меня в работе тоже постоянно возникает желание раздать всем нодам общее видение какой-то части мира. Надо учесть что браузер тоже может быть нодой.
                  0
                  Видел ваш project.clj, выносит мозг конечно. У нас попроще:

                  [org.clojure/clojure «1.5.1»]
                  [clj-oauth «1.4.0»]
                  [http-kit «2.1.10»]
                  [ring/ring-core «1.2.0»]
                  [compojure «1.1.5»]
                  [org.clojure/data.json «0.2.2»]
                  [org.clojure/clojurescript «0.0-1889»]
                  [prismatic/dommy «0.1.1» :exclusions [crate prismatic/cljs-test]]

                  и даже вебсокетов нет :(
                    0
                    А как в clojure узнают про выход новых версий? В файле проекта все гвоздями прибито ведь.

                    В node.js, например npm install, и все наглядно
                      0
                      А что, в ноде версии библиотек не указываются, и оно само может обновиться и всё сломать нафиг?
                        0
                        Можно зафиксировать версии для всего дерева зависимостей при желании
                        0
                        [~/Dropbox/ws/kick] lein ancient
                        [clj-oauth «1.4.1»] is available but we use «1.4.0»
                        [http-kit «2.1.11»] is available but we use «2.1.10»
                        [org.clojure/data.json «0.2.3»] is available but we use «0.2.2»
                        [org.clojure/clojurescript «0.0-1909»] is available but we use «0.0-1889»
                        [prismatic/dommy «0.1.2»] is available but we use «0.1.1»
                          0
                          Спасибо!
                        0
                        Да в общем-то всё по делу :) Не ожидал, конечно, что так много получится.

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

                    Самое читаемое