Раздел 28 · Clojure

Сквозной проект: разбор events.log

middle~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Главная мысль урока: весь раздел складывается в одну программу. Строка лога становится мапой, мапы проходят через трансдьюсер за один проход, агрегаты собирают frequencies, group-by и множества, а clojure.test сверяет ответ с цифрами, которые аналитик получил на Python.

В курсе аналитика есть урок про Python-секцию собеседования. Там руководитель агентства Лада дала кандидату events.log «Хроник Пограничья» за две недели после релиза 2.0 и попросила топ событий. Кандидат ответил: чаще всего install (310), tutorial_start (287) и tutorial_complete (170). Аналитик видел игру снаружи, через лог. Ты весь раздел строил её изнутри: героев мапами, бой мультиметодами, мир атомом, сохранения в EDN. Теперь замкнём круг и разберём тот же лог на Clojure.

Вот как выглядят его строки, включая мусор, который туда нарочно подложили:

2026-05-14T07:38:24Z player=2979 platform=ios event=install
2026-05-14T07:44:32Z player=2979 platform=ios event=tutorial_start

2026-05-22T13:0
2026-05-25T10:00:00Z player=1999 platform=ios

Первое слово это время, дальше поля ключ=значение. Пустая строка, оборванная строка и строка без event должны тихо пропускаться.

Строка в мапу

Первая функция пайплайна превращает текст в данные. Поле ищем по имени, а не по позиции: в курсе аналитика line.split()[3] ломался, как только порядок полей менялся.

Деструктуризация [ts & words] отделяет время от полей. keep с when выбрасывает слова без =. Если event нет, функция возвращает nil, и дальше по пайплайну keep выбросит и саму строку. Все решения о мусоре собраны в одном месте.

Топ событий за один проход

Дальше та же задача, что у аналитика. Склеим трансдьюсер «разобрать и взять событие» и посчитаем частоты:

transduce проходит по строкам один раз: каждая строка разбирается, превращается в имя события и сразу попадает в мапу счётчиков. Промежуточного вектора разобранных мап нет. completing добавляет к функции шага арность завершения, которую требует договор трансдьюсера. Сортировка juxt по убыванию частоты и по имени взята из урока про алгопрактику.

На настоящем файле этот код даст install (310), tutorial_start (287), tutorial_complete (170), те же цифры, что в ответе Ладе. Проверишь это сам в первой задаче.

Воронка: где теряются новички

Топ событий говорит, что обучение закончили 170 раз, а начинали 287. На каком шаге теряются игроки, покажет воронка: сколько разных игроков дошло до каждого шага. Строгая воронка считает на шаге только тех, кто прошёл и все предыдущие. На множествах игроков это пересечения:

Игрок 3 начал обучение без установки в окне выгрузки (поставил игру раньше 14 мая), поэтому в строгую воронку не попал. reductions возвращает все промежуточные результаты свёртки: первый шаг, пересечение первых двух, пересечение трёх. Это та же reduce, только с историей.

Дальше воронку режут по платформам через group-by :platform. На всём логе картина такая:

ПлатформаУстановилиНачали обучениеЗакончилиДоля закончивших
android1561385338%
ios82796177%
pc72705680%

На iOS и PC обучение заканчивают около 80% начавших, на Android меньше 40%. Это та же история, что аналитик нашёл на полной базе в уроке про воронки: после релиза 2.0 сломалось обучение Android-сборки. В выгрузке игроков меньше, поэтому проценты другие (там 34,8% против 81-83%), но провал на том же шаге. Посчитаешь таблицу во второй задаче.

Сессии: время как данные

Лог хранит время строкой. Чтобы считать интервалы, её надо превратить в число. Здесь пригодится урок про EDN: тег #inst разбирает ISO-время одинаково в браузере и на JVM.

Сессия это события одного игрока без перерыва длиннее 30 минут. Свёртка идёт по отсортированным меткам времени: если новая метка близко к последней в текущей сессии, она туда добавляется, иначе открывает новую. Вектор векторов, peek и pop работают с концом вектора за O(1).

Когда лог приходит потоком

Пока файл читался целиком. На сервере строки приходят по одной, и счётчики живут дольше одного вызова функции. Тогда агрегат держит атом, а разбор остаётся той же чистой функцией:

Побочный эффект один, swap!, и он на краю программы. Если строки приходят из канала, тот же event-of встаёт в трансдьюсер канала, как в уроке про core.async. А clojure.test из урока про тесты проверяет parse-line и воронку без файла и без сервера.

Попробуй руками

Обе задачи читают настоящий events.log из курса аналитика через (slurp "events.log"). В первой ты разберёшь строку и получишь топ событий, во второй посчитаешь воронку новичков целиком и по платформам. Ответы сверяются с цифрами курса.

Проверь себя

Задание

Дальше

Раздел закончен. Ты прошёл путь от литералов до HTTP-сервиса и пайплайна на трансдьюсерах. Что делать дальше:

  • Книги. «Clojure for the Brave and True» Дэниела Хиггинботэма, если хочется ещё раз пройти основы с юмором. «Programming Clojure» Миллера, Хэллоуэя и Бедры как справочник по всему языку. «Getting Clojure» Расса Олсена про то, как думать на Clojure. «Elements of Clojure» Закари Теллмана про имена, абстракции и границы, когда код уже пишется, а хочется писать его лучше.
  • Библиотеки. reitit для маршрутов и malli для схем из урока про схемы, next.jdbc для базы данных, integrant или component для сборки системы из частей, shadow-cljs для фронтенда.
  • Сообщество. Каналы Clojurians в Slack, форум ClojureVerse, страница сообщества на clojure.org. Там отвечают на вопросы новичков и обсуждают библиотеки.

А если захочется второго лиспа с другой ставкой, раздел про Janet покажет, как такой язык устроен изнутри: парсер, байткод, файберы.

домашка

Домашка