Главная мысль урока: весь раздел складывается в одну программу. Строка лога становится мапой, мапы проходят через трансдьюсер за один проход, агрегаты собирают
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. На всём логе картина такая:
| Платформа | Установили | Начали обучение | Закончили | Доля закончивших |
|---|---|---|---|---|
| android | 156 | 138 | 53 | 38% |
| ios | 82 | 79 | 61 | 77% |
| pc | 72 | 70 | 56 | 80% |
На 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 покажет, как такой язык устроен изнутри: парсер, байткод, файберы.
домашка