-
Clojure Coding Guide
TL;DR: this is a detailed description of how to write good Clojure code. It’s based on my 8 years of experience with Clojure for both commercial purposes and side projects as well. Some parts of this document repeat the well-known guide by Bojidar. Other parts instead break the conventional rules in Clojure development. For such cases, I give an explanation of why they are what they are. Everything written below has come from practice, and I hope you’ll find it useful.
Table of Contents
- Parentheses
- Namespaces
- Variables and defs
- Atoms
- Exceptions
- Assertions
- Functions
- Arguments
- Destructuring
- Keywords
- Keyword processing
- Type hints
- Naming
- Lines and indentation
- Maps
- Let
- Case, cond
- Macros indentation
- Java interop
- Java-like classes
- Reuse Java classes
- Systems and Components
- Collections
- Clojure.spec
- Types & Records
- Named sections
- Comments
- Trailing spaces
- Commented code and dev sections
- Tests
- Core.async
- Amazonica
- Libraries
Parentheses
Let’s start with something obvious yet worthy of repeating. When writing Lisp code, don’t balance parentheses as you did before in Python or JavaScript. Not like this:
(time (doseq [a [1 2 3]] (let [b (* a a)] (println b) ) ) )But this:
(time (doseq [a [1 2 3]] (let [b (* a a)] (println b))))The same rule applies to collections. The following code fragments look weird in Clojure:
(def mapping { :name "Ivan" :email "test@test.com" }) (def numbers [ 1, 2, 3 ])It is a strong rule for every Lisp dialect and you’ve got to get on with it. The sooner you get an editor powered with a plugin to manage parenthesis the better it is for you as a programmer. Emacs + Paredit is a good choice but it’s a matter of preference.
-
Кто виноват
Наверное, вы слышали: чтобы получить правильный ответ, задавайте правильный вопрос. И наоборот: неправильный вопрос займет массу времени и сил, но ответа не даст. Удивительно, что многие вопросы, что мы задаем, поставлены неверно. Один из них звучит как в заголовке — кто виноват?
Стоит чуть-чуть подумать, как станет ясно, что это неправильный вопрос. Так ли важно, кто именно виноват? Что это дает? Как виновность поможет исправить ситуацию и предотвратить ошибку в будущем?
Если взять историю любой катастрофы — Титаник, Чернобыль — везде окажется, что виноваты не люди на местах. Это очень желанно руководству, особенно если исполнитель умер. Непрофессиональные операторы взорвали реактор. Беспечная команда нашла на айсберг, и делу конец.
Да что катастрофы, вспомним истории с падением прода. Всегда оказывается, что программист Вася запустил опасный скрипт с кредами от прода, которых у него быть не должно. Креды дал ему под честное слово админ во время попойки в баре. Креды выдаются по заявке, нужно собрать пять подписей, но гендиректору некогда, его зам на больничном и так далее.
Когда сотрудник Гитлаба удалил базу с прода, выяснилось много вещей. Бекапы не работали, оповещения об ошибке бекапов тоже не работали. Пока петух не клюнул, никто не волновался.
Какой-то чел из системы очистки воды оставил штат без питья на неделю. Оказалось, он подключался из дома к рабочей машине по TeamViewer, чтобы не ездить по ночам. Десять раз писал начальству с просьбой сделать доступ по впн, но просьбы даже не рассмотрели.
Представим теперь, что условного Васю взяли за шкирку и объявили: ты виноват. Кому от этого легче? Даже если Васю уволят, ситуация повторится в будущем.
Верный признак, что вопрос вины неверно поставлен — он дает большой выхлоп. Вася виноват, но… и дальше экраны текста, что на самом деле чего-то там. Так быть не должно.
Вместо “кто виноват” нужно спрашивать “кто несет ответственность”. Это все меняет: оказывается, вина и ответственность — разные вещи, которые делятся между людьми. Виноват один, отвечает другой. Как они разрулят между собой — не наше дело.
Вася уронил прод и он виноват. Но отвечает техдиректор, потому что у Васи в принципе не должно быть доступа к проду.
Оператор взорвал реактор и он виноват. Отвечают те, кто проектировал, вводил в эксплуатацию и тестировал.
Капитан навел корабль на айсберг и он виноват. За нехватку шлюпок, за неумение команды спасти пассажиров отвечает перевозчик.
Пьяный устроил дебош в клубе. Он виноват, но отвечает охрана, которая его пропустила.
Сотрудник слил базу клиентов. Он виноват, но отвечает фирма — юрлицо. Будьте любезны штраф в десять тысяч за каждую строку. Сливы сразу прекратятся.
Водитель в Воронеже сбил насмерть коляску с грудным ребенком. Оказалось, у него десятки неоплаченных штрафов за последние полгода. Он виноват. Но пусть начальник ГИБДД объяснит, почему водителю не аннулировали права.
Правило: когда случилось что-то из ряда вон, ищите ответственного, а не виновного. Когда найден первый, второй появится автоматически. Но не наоборот: чаще имеем виновного Васю, с которого нечего взять, а ответственных нет.
Бывает, что ответственный известен, но сыскать с него ничего нельзя. Это значит, игра заведомо проиграна. В следующий раз не ведитесь.
Сказанное имеет прямое отношение к сегодняшней ситуации. В России часто ищут виновных и находят их в лице американских президентов, англосаксов, оппозиции, инагентов и бог знает кого еще. Каждый раз, когда это слышу, в голове вопрос: ладно, пусть виноваты англосаксы, но кто несет ответственность? Если никто, что толку мне знать?
-
Интернет и геополитика
На заре интернета было забавное по текущим временам мнение. Якобы он станет средой, которая объединит людей, отбросив границы и политические взгляды. Что противоречия физического мира отпадут, и все найдут согласие. Что коллективный разум возобладает над политиками и превзойдет их.
Забавно видеть, что все из этого не только не сбылось, но и наоборот — интернет все больше становится геополитизированным. Увы, в нашу жизнь пришла геополитика — смесь двух терминов, каждый из которых тащит худшее прошлого века: политику и территории. Что из этого вышло, мы наблюдаем сегодня.
Современный интернет геополитизирован, потому что регулируется двумя принципами: политикой и географией. Крупные сервисы блокируют людей по их положению на планете. Страны блокируют сервисы по политическим причинам. Большая часть запрещенного контента — это то, что запрещено режимом, причем не только в России.
Тезис о едином пространстве разваливается на глазах. Все меньше остается сервисов, куда можно попасть, не испытывая ограничений. Твиттер(1) и Инстаграм(2) заблокированы Россией. На Госуслуги и Налог.ру нельзя зайти, если ты не в России. Если уехал, ставь московский ВПН, что до недавнего времени звучало анекдотом.
Порнхаб в одной стране разрешен, в другой запрещен, в третьей — только с авторизацией через местную соцсеть. Grammarly не работает с российскими IP, нужен ВПН. Украинские сайты показывают россиянам плашку с расчлененкой.
Надоели сайты, защищенные Cloudflare. При каждом заходе видишь уродский экран с задержкой в 5-10 секунд. Иногда Cloudflare требует капчу с гидрантами и велосипедами. Двадцать минут — и сессия рвется, проверка начинается снова.
Гугл и Ютуб боятся ВПН как огня. С ним Ютуб показывает капчу — не дай бог ты пройдешь мимо Большого Брата. Фейсбук, Твиттер и аналоги блокируют Тор. Им лучше знать, что безопасно, а что нет.
Про Китай писать нет смысла.
Сервисы все больше ведут себя как государства. Пользователь, чей IP вызывает малейшее подозрение, приравнивается к нелегалу, незаконно переходящему границу. Его надо схватить и подвергнуть деанону.
Фейсбук и Твиттер открыто занимают позиции в социальных конфликтах США: выборы, BLM и так далее. Получить бан за поддержку Трампа теперь в порядке вещей. Не лучше ситуация и у нас, когда ВК и Мейл.ру сливают данные о тех, кто состоит в группах “оппозиции”.
В той или иной мере огораживается каждый второй сайт. Со включенным ВПН нельзя передать показания в энергослужбу или заказать б/у книгу. Знакомый справедливо заметил, что сегодня проще иметь два ноута — настроенный под иностранный сайты и для внутренних нужд.
Выделять какую-то страну в том плане, что они блокирую больше, чем остальные, нет смысла — это тренд. Интернет — это общение и информация, и государство всегда болезненно реагирует, когда то или другое выходит из под контроля. И неважно, какой политический строй: демократия, автократия или тоталитаризм. Благородный предлог найдется всегда: терроризм, антиваксы, тридевятая колонна. В каждой стране свои пугалки про черную руку и простыню.
Очевидно, никакого свободного интернета нет, более того — не интернет двигает обществом, а двигают им. Современная дурь оседает в интернете и приводит к ограничениям по IP, странам, действиям режима и остальному.
Получится ли сделать что-нибудь другое? Возможно, но это будет принципиально иной вид связи. Он должен быть максимально простым для пользователя и невозможным для блокировок. Примерно как блокчейн, хоть я и не люблю аналогии с криптой. К сожалению, лично мне этот новый вид связи трудно представить хотя бы приблизительно.
1, 2 – считаются экстремистскими организациями или вроде того.
-
Мобильная деградация
Последнее время я делю экран пополам: слева браузер, справа Емакс. Такой сетап нужен для определенной работы, связанной с редактированием текста.
Заметил, что с браузером происходят странные вещи. Стоит только поджать окно на жалкие пиксели, включается мобильный режим. Лучше всего это наблюдать на Гитхабе. Нормальный режим:

Но стоит уменьшить окно на пиксель, как вдруг:

Кто-нибудь в курсе, зачем это? Код — самое главное на этой странице — пропал под выпадашку, хотя места достаточно. Появилась адская разреженность, кнопки сплющились, как будто по ним проехали катком. Кто вообще просил? Остается выругаться и расширить окно обратно.
Досадно от того, что кто-то сидел над этой хитрожопой версткой, трекал часы в Джире, презентовал менеджерам. А пользователь мечтает развидеть и вернуть все взад.
-
Автодополнение
В хромо-подобных браузерах есть полезная функция — сохранение форм. Дико выручает, когда в сотый раз вводишь имя, фамилию и почту на сайте. Буквально сегодня я заполнял форму на сайте налоговой шестнадцать(!) раз. Если бы не автокомплит, я бы, наверное, стер пальцы. Тем более умники-разработчики повы..бывались с некоторыми полями, и в них не работает вставка. Какой-то скрипт блокирует событие, я начал было смотреть в консоли, но в итоге плюнул на это.
Удивляет, однако, то, что автокомплит нельзя редактировать. В очередной раз я вставил не то значение, и оно осталось в выпадашке навсегда. Теперь каждый раз, выбирая из нее пункт, надо быть осторожнее, чтобы не тыкнуть не туда.

Не понимаю, почему автокомплит нельзя редактировать? Хватило бы удаления: напротив каждого элемента сделать крестик, который удаляет его из выпадашки. Просто и дешево.
Между прочим, такая функция есть в Телеграме. Предположим, вы искали сообщество LaTeX, а нашли латекс и доминирование:

Если тыкнуть в любой канал (ну, просто так), он останется в истории поиска. Будет неприятно, если это всплывет на созвоне при шаринге экрана. Крестик справа удаляет элемент из истории поиска, и с ним я спокоен: никакого доминирования мой собеседник не увидит.

В общем смысле это ведет к правилу: собираешь данные — позволь клиенту их удалить. Удаление стоит усилий, порой огромных (привет GDPR), но тогда незачем и начинать.
-
Win-win
Читаю книгу Егора “Наш код”. Хорошая книга, местами спорная, но ничего — просто держите в голове, что это Егор. Есть, однако, момент, мимо которого я не мог пройти молча — это эпизод с переговорами между двумя группами.
Я хотел привести оригинальный текст, однако электронной версии, откуда можно скопировать и вставить, в интернете нет. Поэтому расскажу завязку своими словами.
Есть группа разработчиков, которые пилят REST API, и есть группа потребителей. Понадобилась доработка сервиса, и потребители выдвигают условие: чтобы новые данные можно было забрать одним запросом. Разработчикам сервиса это неудобно: они сделали несколько новых методов, полагая, что потребители пошлют несколько запросов. Группы не сошлись во мнении, и начинается совещание.
-
Программа Preview
Если я когда-нибудь пересяду с Мака на что-либо другое, больше всего мне будет не хватать… программы Preview. Да, встроенной программы просмотра изображений. Все потому, что Preview — невероятно мощный софт. Просмотр картинок и PDF лишь малая часть ее возможностей. Ниже я расскажу, что именно она умеет.
(1) Банально, но все-таки — Preview открывает картинки и PDF. Не нужно ставить сторонний софт, который пропишет себя в автозагрузку и службы. С тяжелым сердцем вспоминаю Adobe Reader, который разросся до размера офисного пакета.
Поставив Windows 10 на игровой комп, был удивлен, что в ней нечем открыть PDF (браузер Edge не в счет). По клику на PDF открывается магазин Microsoft – как это возможно в 2022 году, не понимаю. А на Маке сел — и читаешь PDF.
-
Коротко об lndir
Небольшая заметка о утилите, которая требуется редко, но метко –
lndir.Когда у меня стало больше одного Мака, появилась проблема синхронизации настроек. Другими словами, чтобы всякие
.thisrcи.thatrcбыли одинаковы и подхватывались при изменении. Сюда входит конфиг Emacs, ssh, словари aspell, профили AWS, конфиги ctags, zshell и многое другое.Легче всего держать dot-файлы в репозитории и ставить симлинки. Неожиданно я столкнулся с тем, что не так легко написать shell-скрипт, который бы поставил симлинки на файлы из папки. Пытался при помощи
find ... -executeиxargs, но постоянно что-то мешало. То отсутствие файла, то его существование, словом, всякие досадные случаи. В итоге было две команды: сначала одна удаляет симлинки, а вторая создает их заново.Оказалось, что
lndirделает именно то, что я искал. Они принимает две папки и строит дерево симлинков из первой во вторую. При этом учитывает случаи, что я перечислил, например если симлинк уже есть. Благодаря этому дерево можно обновлять итеративно.Пример: в каталоге
~/work/System/Dotfilesхранятся оригинальные файлы Emacs,.sshи прочие. Следующая команда make расставит симлинки в домашнюю папку:HOME = /Users/ivan PWD = $(shell pwd) create-symlinks: lndir ${PWD}/Dotfiles ${HOME} chmod 600 ${HOME}/.ssh/*Для файлов ssh необходимо выставить права 600, иначе утилита ругается.
Синхронизация происходит обычным способом через git. Как только вы поменяли один из файлов, делаете коммит и пуш. На другой машине пулл и
make create-symlinks, и все подхватывается. Репозиторий, понятно, должен быть приватным.По умолчанию
lndirнет в поставке Линукса и Мака. Ставится из привычныхaptиbrew. -
Интерфейс Гитхаба
У меня бомбит от интерфейса Гитхаба. Он работает по странному принципу: показывает то, что не нужно и не показывает то, что нужно.
Когда я открываю pull request, то хочу увидеть изменения в файлах. Поскольку я обычный программист, не гений и без заскоков, полагаю, это желание подходит большинству. Почти всегда, когда мне кидают PR, я знаю заранее, с какой задачей он связан, и сразу смотрю файлы. Только в редких случаях мне нужно прочесть описание.
В интерфейсе PR файлы задвинуты на последнюю вкладку “Files changes”, и я не верю, что этому есть разумная причина. Файлы это суть PR, с какой стати задвигать их в конец? Это же самое нужное! Изменения должны быть сразу под описанием, чтобы не кликать, а просто смотать экран.

Есть ли на этой вкладке хоть грамм полезной информации? Если да, чем он важнее 18 измененных файлов?
Далее эти дурацкие табы. Проблема в том, что на вкладках располагаются не только данные, но и кнопки. Например, закрыть PR можно только со вкладки “Conversation”. А поставить аппрув только со вкладки “Files”. В итоге постоянно кликаешь на первый и последний табы.
Первый и последний, Карл! Уже это говорит, что как минимум они должны быть рядом, если не объединены в один.
По этой причине я пользуюсь трюком: когда кидаю ссылку на PR, добавляю к концу
/files, то есть не...project/pull/3, а...project/pull/3/files. При таком раскладе у человека сразу откроются файлы, и не придется переключать табы. Мелочь, а приятно, особенно если собеседник понимает этикет и отвечает тем же.Судите сами: вот мне пришел PR с комментарием. Рассмотрим действия, которые я должен выполнить для мерджа:
- открыть ссылку на PR;
- перейти на files, чтобы бегло посмотреть, что внутри;
- если все хорошо, вернуться на вкладку conversation;
- нажать merge;
- нажать delete branch.
По мне все можно уместить на одной странице и работать без кликов.
Из-за особенностей интерфейса кнопка “delete branch” находится выше “merge”, то есть сначала нажимаешь ту кнопку, что ниже, а потом выше. Это ни в какие ворота: экран мотают сверху вниз, и чем ниже кнопка, тем она важнее.

В целом интерфейс Гитхаба шумный и грязный. На закладке “Conversation” схематично указаны коммиты, хотя есть отдельная вкладка “Commits”. Зачем размазывать их по двум вкладкам? Все, что касается коммитов, должно быть в “Commits”.
Даже если у PR нет описания, будет пустой комментарий вида:
igrishaev commented 20 minutes ago No description providedСпрашивается, что именно commented? Зачем писать о том, чего нет?
Особую грусть вызывает мобильное приложение Гитхаба. Почему-то оно не может нормально показать код: кнопка “Browse code” вечно болтается внизу. Что мешает сделать ее первой? В этом плане Гитхаб напоминает современный Дропбокс: приложение плохо показывает то, с чем работает.
Однажды я хотел закрыть PR со спамом, но не нашел кнопку “Close pull request”. Возможно, она была под выпадашкой или вроде того, но увы, я пас.

Слева: где посмотреть код? Справа: как закрыть PR?
Мне кажется, интерфейс Гитхаба разжирел, и пора устроить ему чистку. Он достиг стадии “впихнуть невпихуемое”, как это бывает с продуктами, которые часто выкатывают фичи. Интерфейс должен щадить пользователя: не вываливать на голову все подряд в надежде, что кому-то пригодится. Интерфейс — это оборона пользователя от того ада, что творится на серверах. Давайте помнить об этом, хоть я и не верю, что дизайнеры Гитхаба меня услышат.
-
Деджаваскриптизиция (4)
Пора заканчивать эпопею про избавление от Js. Чтобы не утомлять, расскажу о последнем штрихе — как внедрил капчу для комментариев.
Как только я убрал Disqus, полезли спамные комментарии. Каждый день приходят два-три предложения купить виагру, надувную лодку или просто левые ссылки. Поскольку каждый комментарий открывает PR в репозиторий, все остается в истории Гитхаба. Посмотреть на это добро можно по ссылке.
Разгребать подобные комментарии нет желания, поэтому должна быть минимальная защита от спама. С условием — без Js. Надумал такую схему:
- капча генерируется на этапе сборки блога. На выходе получается HTML-форма с полем captcha и значением 2 × 5.
- В форму добавляется поле для решения.
- Сервер парсит капчу, решает и сверяет с ответом. Если что-то не так, заворачивает комментарий.
Как ни странно, даже на таком примитиве боты отваливаются. Разве что с оговоркой: когда был оператор +, боты решали капчу. Как только заменил на
×(знак умножения в юникоде), стала тишь да благодать. Надеюсь, читатель не забыл таблицу умножения! Тестируя форму, сам подвис с примером8 × 9.Техническая сторона: вот построить капчу в шаблоне:
{% assign val1 = '1 2 3 4 5 6 7 8 9' | split: ' ' | sample %} {% assign val2 = '1 2 3 4 5 6 7 8 9' | split: ' ' | sample %} {% assign op = '×' | split: ' ' | sample %} {% assign captcha = val1 | append: " " | append: op | append: " " | append: val2 %}Замечу, что при каждой сборке блога значения будут разные.
Скрытое поле в форме:
<input required name="captcha" type="hidden" value="{{ captcha }}">Виджет для ввода решения:
<div class="block"> <span class="comment-form-label"><small>{{ captcha }} = </small></span> <input required id="comment-form-solution" name="solution" type="text"> </div>Наконец, серверный код проверки капчи:
(dеfn validate-captcha [captcha solution] (when-let [[_ val1-raw op-raw val2-raw] (re-find #"^(-?\d+) (.+?) (-?\d+)$" captcha)] (let [val1 (Integer/parseInt val1-raw) val2 (Integer/parseInt val2-raw) op (case op-raw ("+" "+") + ("*" "×" "×") * nil)] (when (and val1 val2 op) (= (str (op val1 val2)) (str/trim solution))))))Грубо, неуклюже, но работает.
На этом я закончу тему с избавлением от Js. На мой взгляд, цели достигнуты, жить с новыми комментариями можно. Это был интересный опыт, в будущем пригодится.