• Деджаваскриптизиция (3)

    Сбылась мечта идиота: теперь на сайте работают комментарии без строчки кода на Джаваскрипте. Смотайте экран вниз, там форма. Требуется только имя и тело комментария. Ввели, отправили, и через некоторое время он появится в блоге.

    Форма:

    Экран возврата:

    А что, мне нравится.

    Расскажу, как это работает. В прошлый раз я перенес комментарии Disqus в репозиторий. Каждый комментарий — это файл формата YAML + markdown. В числе прочего он хранит ссылку на пост. Когда я собираю блог, в подвал каждой заметки подставляются ее комментарии.

    Для приема комментариев я следую тому же принципу: чтобы они появились на сайте, нужно создать файл в репозитории. Теоретически любой может оформить pull request, но это сложно. Должен быть сервис, который преобразует ввод пользователя в pull request для блога. Этот сервис я написал и назвал blog-backend.

    Сервис напоминает веб-приложение: оно принимает HTTP-запрос с формой. После прелюдии с валидацией перехожу к главному: интеграции с Гитхабом. Это оказалось не так-то просто. У Гитхаба уже три рестовых API, но ни одно не покрывает все возможности. Кроме REST есть убер-апишка на GraphQL — она-то мне и нужна.

    Интеграция с GraphQL была нелегкой: это самобытный язык и вещь в себе. Если бы в текущем проекте не было GraphQL, я бы полез на стенку. Во-вторых, сложность процесса: чтобы сделать PR силами API, нужно четыре вызова:

    1. получить метаданные репозитория, в том числе последний коммит;
    2. создать ветку от этого коммита;
    3. сделать коммит в новую ветку;
    4. сделать PR.

    Как по мне, апишка не консистентна: где-то на репозиторий ссылаешься по имени, а где-то по машинному ID, который приходит из метаданных. То же самое с ветками: где-то имя, где-то айдишник. Ответы GraphQL развесистые, с глубокой вложенностью. Например, последний коммит ветки погружен на пять уровней:

    (-> resp-get-repo :data :repository :ref :target :oid)
    

    С такой апишкой невозможно программировать без образца данных.

    Когда подружился с Гитхабом, настала проблема хостинга: где и как размещать логику. Мне понравился опыт с облаком Яндекса, и я продолжил эксперименты. Сервис написан на Кложе, скомпилирован Граалем в бинарь и хостится в Яндекс.Функции — аналоге AWS Lambda.

    В свою очередь, Яндекс.Функция — это облачный сервис, где размещают какой-то код и дергают его по запросу. Отличие от обычного хостинга в том, что у лямбды нет состояния: она запускается на короткое время и умирает. В зависимости от окружения лямбда может умереть не сразу, и если ее дернуть повторно, сработает уже запущенный экземпляр. На базе этого делают “прогрев” лямбд, у которых окружение стартует медленно, например Java. В моем случае граальный бинарник быстрый как не весть что, прогрева ему не надо.

    Лямбду можно вызывать разными способами, в том числе HTTP-запросом. У каждой лямбды свой урл, который может быть приватным и публичным. Его можно указать в разных сервисах, что отлично подходит для ботов.

    По сравнению с VM-хостингом у лямбды следующие плюсы:

    • не нужна виртуальная машина и ее настройка;
    • не нужен домен;
    • лямбды экстремально дешевы, потому оплачивается только время работы с округлением до 0.1 секунды;
    • лямбды поощряют писать код без компонентов и состояния, что как бальзам на душу после проектов по работе.

    Поначалу я был крайне скептичен к лямбдам, но теперь вижу в них особую прелесть. Единственный момент — пришлось написать код, чтобы подружить Кложу и Ring с окружением лямбды. Также поправил компиляцию Граалем, чтобы в бинарнике была верная кодировка. В будущем я планирую вынести код Яндекс.Облака в отдельную библиотеку, чтобы избавиться от копипасты.

    Форма поддерживает синтаксис Markdown. Пока что нет кнопок выделения текста болдом и италиком, сделаю потом. Форма отправляется чистым POST, никаких аяксов, хотя первую версию я сделал на fetch, JSON и CORS. Про CORS я все забыл, и пришлось читать свою же статью о том, как все настроить.

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

    Первые комментарии я получил от кого? Правильно, от спамеров. Этим утром предложили виагру, казино и просто левые ссылки. Вот ссылка на уже закрытый RP и его содержимое:

    ---
    id: 1663659055173
    is_spam: false
    is_deleted: false
    post: /bookshelf/
    date: 2022-09-20 07:30:55 +0000
    author_fullname: 'epiehuliqukib'
    ---
    
    http://slkjfdf.net/ - Tatacatay <a href="http://slkjfdf.net/">Aebaluk</a>
    lwk.wlgg.grishaev.me.mrq.mn http://slkjfdf.net/
    

    Очевидно, нужно вводить капчу для защиты от ботов. Я рассматриваю это как новый вызов: как сделать капчу без Javascript в статичном блоге? Уже есть мыслишки, расскажу в четвертой части эпопеи.

    Наконец, последний тезис — зачем вы это делаете, мистер Андерсон? Зачем эти отказы, костыли, превозможения? Ответ — потому что могу. Просто хочется экспериментов. Надоели тормозные сайты с JS, хочу маленький оазис в своем огороде.

  • REPL, Cider, Emacs (часть 4/4)

    Все части

    Оглавление

    Отладка в Cider

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

    Теперь когда вы знакомы с самодельным отладчиком, рассмотрим, что предлагает Cider. В нашем распоряжении два тега: #break и #dbg. Первый тег означает точку останова (брейкпоинт) в месте, где он расположен. Поставьте #break в середину произвольного кода. Перед тем как запустить код с тегом, выполните его командой cider-eval-..., иначе эффект не вступит в силу.

    Тег #break ссылается на функцию breakpoint-reader из модуля cider.nrepl.middleware.debug. Она добавляет в метаданные формы поле, которое указывает, что форма подлежит отладке. Далее сработает оснащение (или инструментирование) — алгоритм, который ищет в коде отмеченные формы и добавляет отладку.

    Когда оснащенный код запущен, в нужных местах он прерывается, и от клиента ожидают команду. Можно узнать локальные переменные, выполнить произвольный код или продолжить исполнение. Так продолжается до тех пор, пока оснащенный код не выполнен целиком.

    В Emacs нет графических средств для отладки. Информация выводится либо рядом с кодом, либо в отдельных буферах. В режиме отладки файл нельзя редактировать. Нажатие на клавиши вызывает особые команды. Ошибка новичков в том, что, попав в отладку, они нажимают все подряд, и процесс протекает с ошибками. Ниже мы рассмотрим процедуру так, чтобы с вами этого не случилось.

    Read more →

  • Деджаваскриптизиция (2)

    Сделал то, что намеревался: удалил с сайта все сторонние сервисы. Больше нет социальных кнопок, гугло-аналитики и, самое важное, комментариев Disqus. На страницах вообще нет аякса за тем исключением, когда встроен плеер Ютуба. Красота.

    Отдельно расскажу про комментарии. Теперь они встроены в блог, являются его частью, а не лежат на серверах Disqus. Для этого я написал импорт из XML. Ясное дело, на Кложе, исходники на Гитхабе.

    Если вкратце, код пробегает по XML, строит мапы и индексы по ID. Для каждого комментария он создает файл вроде _comments/2016-08-08-08-27-14.md с содержимым:

    ---
    id: 2826354744
    is_spam: false
    is_deleted: false
    post: /interview/
    date: 2016-08-08T08:27:14Z
    author_fullname: 'Александр'
    author_nickname: 'VinokurovAlexnader'
    author_is_anon: false
    ---
    
    <p>Вопрос "Есть кортеж из трех элементов. Назначить переменным a, b, c его значения".</p>
    <p>У Вас ответ:<br>a, b, c = [1, 2, 3]</p>
    <p>Но это список, а не кортеж. С кортежом будет вот так:<br>a, b, c = (1, 2, 3)</p>
    

    Всего таких файлов 993. В шаблон заметки я добавил код для вывода комментариев:

    {% assign comments = site.comments | where:'post', include.permalink %}
    {% if comments.size == 0 %}
    <center>Комментариев пока нет</center>
    {% else %}
    <center>Комментарии</center>
    <div class="comments-block">
      {% for comment in comments %}
        <div class="comment-block">
          <p class="comment-lead">
            <small>
              <em>{{ comment.author_fullname or comment.author_nickname }}, {{ comment.date | date_to_string: "ordinal", "RU" }}</em>
            </small>
          </p>
          <div>{{ comment.content | markdownify }}</div>
        </div>
      {% endfor %}
    </div>
    {% endif %}
    

    Получилось неоптимально, потому что на каждый пост происходит линейный поиск. В идеале нужно один раз построить словарь URL => список комментариев и затем брать оттуда. Но я не настолько хорошо знаю Jekyll и Ruby, чтобы это запилить.

    Ради интереса проверьте страницы, где много комментариев, например про сервис IVI или про интервью. Лично мне результат нравится: чистенький HTML, минимум CSS, все лаконично. На текущий момент не учитывается структура, но технически это возможно: каждый комментарий знает ID родителя, и однажды их вывод можно улучшить.

    Теперь главное: как сделать прием комментариев. Мне накидали советов, плюс я гуглил, и все сводится двум решениям: Гитхаб или self-hosted сервис.

    Напомню, как работает Гитхаб: люди пишут комменты к какому-либо issue. Зная номер issue, легко выгрести комментарии GET-запросом, сгенерить HTML и вставить под заметкой. Выглядит красиво:

    , но меня тревожат две вещи.

    1. Комментарии лежат в чужом сервисе, который может отвалиться из-за санкций или Роскомнадзора.
    2. Обязательна авторизация через Гитхаб — не у всех есть учетная запись.

    Self-hosted-решения вроде Remark42 интересны, но вынуждают держать виртуалку, платить за нее и делать бекапы. Придется возиться.

    Я придумал так. Пишем лямбду для Яндекс.Облака (на Кложе с компиляцией Граалем). Лямбда принимает POST-запросы с сайта. На сайте висит статичная форма с полем action=URL нашей лямбды. Отправить форму может любой желающий, даже тот, у кого нет учетки в Гитхабе, Твиттере, Инстаграме и так далее.

    Получив запрос, лямбда создает pull request в репозитории блога. Для этого не нужен git: все возможности Гитхаба доступны в REST API. Pull request содержит новый файл с комментарием в папке _comments. Мне приходит письмо. Если норм, я нажимаю Merge, блог собирается и выкатывается новая версия.

    Одновременно получается модерация: если кто-то написал дичь, я отклоняю PR, и делу конец.

    Открыв PR, лямбда перенаправляет пользователя обратно в блог со словами “спасибо, ваш комментарий скоро появится”.

    Вот такую штуку я хочу запилить. А пока что комментации работают только в Телеграме.

  • Деджаваскриптизиция

    Как вы думаете, сколько запросов делает браузер, пока вы читаете заметку в моем блоге? Я тоже не знал. Десять, двадцать? Что гадать, если можно проверить!

    Открываем консоль разработчика, вкладка Network. Виден всякий ajax/js-хлам. Тык правой кнопкой → “Save all as HAR with content”:

    Получаем жирный JSON со всеми запросами. Вот как выдрать из него урлы:

    cat grishaev.me.har | jq '.log.entries[].request.url' | sort > urls.txt
    

    Если открыть urls.txt и удалить адреса с доменом grishaev.me (коих 5 штук), получится… 69 запросов. Вот они:

    https://a.disquscdn.com/1662551939/images/noavatar92.png
    https://a.disquscdn.com/1662551939/images/noavatar92.png
    https://accounts.google.com/o/oauth2/iframe
    https://accounts.google.com/o/oauth2/iframerpc?action=checkOrigin&origin=https%3A%2F%2Fdisqus.com&client_id=508198334196-bgmagrg0a2rub674g0shidj8fnd50dji.apps.googleusercontent.com
    https://accounts.google.com/o/oauth2/iframerpc?action=issueToken&response_type=token%20id_token&login_hint=AJDLj6J0ayD55kiHpX6FEyK8AjchAx-IxO13rW-0myyrGzenEj7YZFquZJE4XTK6YDPt86Y7SJkVEXs3QpNMeay0bCjiNqDIwg&client_id=508198334196-bgmagrg0a2rub674g0shidj8fnd50dji.apps.googleusercontent.com&origin=https%3A%2F%2Fdisqus.com&scope=profile%20email&ss_domain=https%3A%2F%2Fdisqus.com&include_granted_scopes=true
    https://apis.google.com/_/scs/abc-static/_/js/k=gapi.lb.en.z9QjrzsHcOc.O/m=auth2/rt=j/sv=1/d=1/ed=1/rs=AHpOoo8359JQqZQ0dzCVJ5Ui3CZcERHEWA/cb=gapi.loaded_0?le=scs
    https://apis.google.com/js/api.js
    https://c.disquscdn.com/get?url=https%3A%2F%2Fgrishaev.me%2Fassets%2Fstatic%2Faws%2Fbw%2Fui.png&key=5su7aFyKRIY_tqfWlxdSzw&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fgrishaev.me%2Fassets%2Fstatic%2Faws%2Fmail%2F2.png&key=P-FCJdQOuptvsG613K8vJA&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Fexcel%2Fgood.jpg&key=8OltNCXozdgFxMHmgBTDtQ&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Ftele%2Fscan.jpg&key=3S4NMvR9Sa2dqAik9FSSBw&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Fvlc%2Feat.png&key=_fOEu4w6cw03wCawGGrA_Q&h=200
    https://c.disquscdn.com/next/current/embed/lang/ru.js
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/funny-512x512.png
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/sad-512x512.png
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/upvote-512x512.png
    https://c.disquscdn.com/next/current/recommendations/lang/ru.js
    https://c.disquscdn.com/next/embed/alfie_v4.63f1ab6d6b9d5807dc0c94ef3fe0b851.js
    https://c.disquscdn.com/next/embed/assets/font/icons.4cc7a703d2fdfe684151ff8ac24d45f1.woff2
    https://c.disquscdn.com/next/embed/assets/img/disqus-social-icon-dark.a621bea3e02c9fa04fd3965a3d6f424d.svg
    https://c.disquscdn.com/next/embed/assets/img/loader.ba7c86e8b4b6135bb668d05223f8f127.gif
    https://c.disquscdn.com/next/embed/assets/img/sprite.ad630a07080a45451f139a7487853ff8.png
    https://c.disquscdn.com/next/embed/assets/img/svg-sprite.4da5413f5086c5755b46094b813dbfcd.svg
    https://c.disquscdn.com/next/embed/common.bundle.33bc87b2c4f9324203cc85b7dd1d0492.js
    https://c.disquscdn.com/next/embed/common.bundle.33bc87b2c4f9324203cc85b7dd1d0492.js
    https://c.disquscdn.com/next/embed/lounge.bundle.8d28276e15f31af0eebfd934278922d1.js
    https://c.disquscdn.com/next/embed/lounge.bundle.8d28276e15f31af0eebfd934278922d1.js
    https://c.disquscdn.com/next/embed/lounge.load.0837a7fb2afa86b68e4ee5098ec9905b.js
    https://c.disquscdn.com/next/embed/styles/lounge.4ceaf0673822a0def820ebdc38d84415.css
    https://c.disquscdn.com/next/embed/styles/lounge.4ceaf0673822a0def820ebdc38d84415.css
    https://c.disquscdn.com/next/recommendations/assets/img/img-placeholder.df52e7638153b73862008d3d0556fdda.png
    https://c.disquscdn.com/next/recommendations/common.bundle.a59fbd11efae764ccd959d61e4925fee.js
    https://c.disquscdn.com/next/recommendations/common.bundle.a59fbd11efae764ccd959d61e4925fee.js
    https://c.disquscdn.com/next/recommendations/recommendations.bundle.926bc472e4859a48daa346b4ba2ab4f4.js
    https://c.disquscdn.com/next/recommendations/recommendations.bundle.926bc472e4859a48daa346b4ba2ab4f4.js
    https://c.disquscdn.com/next/recommendations/recommendations.load.9d352c9674ae8172f8669d3aa3a905e9.js
    https://c.disquscdn.com/next/recommendations/styles/recommendations.10022a97346f1c6e3798931bbd8e4bb5.css
    https://c.disquscdn.com/next/recommendations/styles/recommendations.10022a97346f1c6e3798931bbd8e4bb5.css
    https://cdn.viglink.com/images/pixel.gif?ch=1&rn=8.95348561821992
    https://cdn.viglink.com/images/pixel.gif?ch=2&rn=8.95348561821992
    https://connect.facebook.net/en_US/sdk.js
    https://disqus.com/api/3.0/discovery/listRecommendations.json?forum=igrishaev&thread=url%3Ahttps%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&limit=8&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/forums/details?forum=igrishaev&attach=forumFeatures&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/forums/details?forum=igrishaev&attach=forumFeatures&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/threadReactions/loadReactions?thread=9335567925&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/embed/comments/?base=default&f=igrishaev&t_u=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&t_d=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&t_t=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&s_o=default
    https://disqus.com/next/config.js
    https://disqus.com/next/config.js
    https://disqus.com/next/config.js
    https://disqus.com/recommendations/?base=default&f=igrishaev&t_u=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&t_d=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&t_t=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.
    https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i&display=swap&subset=cyrillic,cyrillic-ext,latin-ext
    https://fonts.gstatic.com/s/ptserif/v17/EJRSQgYoZZY2vCFuvAnt66qSVyvVp8NA.woff2
    https://fonts.gstatic.com/s/ptserif/v17/EJRVQgYoZZY2vCFuvAFSzr-_dSb_nco.woff2
    https://fonts.gstatic.com/s/ptserif/v17/EJRVQgYoZZY2vCFuvAFWzr-_dSb_.woff2
    https://glitter.services.disqus.com/urls/?callback=dsqGlitterResponseHandler&forum_shortname=igrishaev&thread_id=9335567925&referer=https%3A%2F%2Fgrishaev.me%2F
    https://igrishaev.disqus.com/embed.js
    https://igrishaev.disqus.com/recommendations.js
    https://io.narrative.io/?companyId=19&id=disqus_id%3Ac6g0c4dk2irns7l&ret=img&ref=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F
    https://links.services.disqus.com/api/ping
    https://live.rezync.com/pixel.html?c=4656c20ee35215f78e9273796625d90b&cid=c6g0c4dk2irns7l&pctry=RU&referrer=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F
    https://mc.yandex.ru/metrika/tag.js
    https://pippio.com/api/sync?pid=1391&ref=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&it=1&iv=c6g0c4dk2irns7l
    https://referrer.disqus.com/juggler/event.gif?abe=1&embed_hidden=0&load_time=1311&event=init_embed&thread=9335567925&forum=igrishaev&forum_id=3964395&imp=2l7278t3kf6iha&prev_imp&thread_slug=zippo_additions_to_the_standard_clojurezip_package&user_type=anon&referrer=https%3A%2F%2Fgrishaev.me%2F&theme=next&dnt=0&tracking_enabled=1&experiment=network_default&variant=fallthrough&service=dynamic&promoted_enabled=true&max_enabled=true
    https://referrer.disqus.com/juggler/event.js?experiment=network_default&variant=fallthrough&page_referrer=https%3A%2F%2Fgrishaev.me%2F&product=embed&thread=9335567925&thread_id=9335567925&forum=igrishaev&forum_id=3964395&zone=thread&page_url=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&service=dynamic&verb=view&object_type=product&object_id=embed&extra_data=%7B%22color_scheme%22%3A%22light%22%2C%22anchor_color%22%3A%22rgb(42%2C122%2C226)%22%2C%22typeface%22%3A%22serif%22%2C%22width%22%3A740%7D&event=activity&imp=2l7278t3kf6iha&prev_imp=&section=default&area=n%2Fa
    https://referrer.disqus.com/juggler/stat.gif?event=lounge.loading.view
    https://www.google-analytics.com/analytics.js
    https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.IdpIFrameHttp.en_US.z2a12LkW96g.es5.O/d=1/rs=AOaEmlHeSnuHrM44y1tAD9SSj44ODEuRFQ/m=base
    https://yastatic.net/es5-shims/0.0.2/es5-shims.min.js
    https://yastatic.net/share2/share.js
    

    Да, Карл: шестьдесят девять запросов на всякие аналитики, кнопки шаринга и комментарии Disqus. Не знаю, почему некоторые урлы повторяются, выяснять это нет никакого желания. Важное уточнение: 69 – это только в начале. Если промотать страницу и написать комментарий, подгрузится еще пачка скриптов.

    С Disqus вообще беда: с недавних пор они внедрили рекламу. Блокировщики справляются, но как-то раз я зашел на сайт из голого Сафари и о…уел от того, что творит Disqus на странице:

    На мобиле еще хуже: баннеры выстраиваются вертикально, и нужно мотать два экрана.

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

    Ясно одно: это не правильно, нужно исправлять.

    Социальные кнопки и гугло-аналитику банально удалю. За кнопки мне стыдно, потому что это рудимент нулевых. Сегодня они встречаются разве что на сайтах, сделанных в говно-CMS вроде Джумлы, Друпала и Вордпресса. Аналитика была б хороша, если б Гугл не плевал на все этические нормы. Выбирая между желанием посмотреть, кто и что читает и приватностью читателей, я предпочту второе.

    Самое интересное с комментариями. Не сказать, что мои заметки активно комментируют, но время от времени это случается. Особо приятно получить комментарий от бывших коллег, с которыми когда-то работал. Несколько раз в комментариях писали ценные вещи, и я был искренне благодарен.

    У меня возникла идея перенести комментарии Disqus в блог. Технически это возможно: в админке Disqus выгружаем все комментарии, получаем жирный XML-файл (про JSON ребята не слышали). Далее пишем скрипт, который обходит XML и дописывает в файлы с постами. Однажды я уже делал так, когда мигрировал с Эгеи (движка на PHP), только вместо XML был дамп MySQL.

    Остался последний вопрос: как сделать прием комментариев без Disqus? У меня есть одна мысль, но я опишу ее в другой раз, а пока что послушаю ваши советы.

  • REPL, Cider, Emacs (часть 3/4)

    Все части

    Оглавление

    Пространства имен

    При работе с REPL мы всегда находимся в каком-то пространстве. По умолчанию оно написано в приглашении, например:

    user=> (+ 1 2)
    

    Если перейти в другое пространство, изменится и приглашение:

    user=> (in-ns 'foobar)
    foobar=>
    

    Каждая форма вычисляется в каком-то пространстве. Если объявить в модуле user переменную:

    (in-ns 'user)
    (def number 1)
    

    , а затем сослаться на нее в пространстве foobar, получим ошибку, что символ number нельзя разрешить в текущем контексте:

    (in-ns 'foobar)
    (+ 1 number)
    
    Syntax error compiling at (repl-chapter:localhost:53495(clj)*:1:8440).
    Unable to resolve symbol: number in this context
    

    Другой пример: объявим в модулях user и foobar переменные number со значениями 1 и 2. Теперь одна и та же форма (inc number) даст разный результат в зависимости от того, какое пространство текущее. Это значит, перед вычислением мы должны убедиться, что находимся в нужном пространстве имен.

    Чтобы уберечь нас от подобных ошибок, nREPL учитывает параметр ns в сообщениях. Когда мы выполняем код при помощи cider-eval-..., в сообщении, помимо полей op и code, передается ns. Его значение Cider находит из формы (ns...) в начале файла. Сервер на время меняет пространство имен, и вычисление протекает в том контексте, что мы ожидаем.

    Read more →

  • Zippo: additions to the standard clojure.zip package.

    The clojure.zip package is a masterpiece yet misses some utility functions. For example, finding locations, bulk updates, lookups, breadth-first traversing and so on. Zippo, the library I’m introducing in this post, brings some bits of missing functionality.

    Installation

    Lein:

    [com.github.igrishaev/zippo "0.1.0"]
    

    Deps.edn

    {com.github.igrishaev/zippo {:mvn/version "0.1.0"}}
    

    Usage & examples

    First, import both Zippo and clojure.zip:

    (ns zippo.core-test
      (:require
       [clojure.zip :as zip]
       [zippo.core :as zippo]))
    

    Declare a zipper:

    (def z
      (zip/vector-zip [1 [2 3] [[4]]]))
    

    Now check out the following Zippo functions.

    A finite seq of locations

    The loc-seq funtion takes a location and returns a lazy seq of locations untill it reaches the end:

    (let [locs (zippo/loc-seq z)]
      (mapv zip/node locs))
    
    ;; get a vector of notes to reduce the output
    [[1 [2 3] [[4]]]
     1
     [2 3]
     2
     3
     [[4]]
     [4]
     4]
    

    This is quite useful to traverse a zipper without keeping in mind the ending condition (zip/end?).

    Finding locations

    The loc-find function looks for the first location that matches a predicate:

    (let [loc (zippo/loc-find
               z
               (fn [loc]
                 (-> loc zip/node (= 3))))]
    
      (is (= 3 (zip/node loc))))
    

    Above, we found a location which node equals 3.

    The loc-find-all function finds all the locatins that match the predicate:

    (let [locs (zippo/loc-find-all
                z
                (zippo/->loc-pred (every-pred int? even?)))]
    
      (is (= [2 4]
             (mapv zip/node locs))))
    

    Since the predicate accepts a location, you can check its children, siblings and so on. For example, check if a location belongs to a special kind of parent.

    However, most of the time you’re interested in a value (node) rather than a location. The ->loc-pred function converts a node predicate, which accepts a node, into a location predicate. In the example above, the line

    (zippo/->loc-pred (every-pred int? even?))
    

    makes a location predicate which node is an even integer.

    Updating a zipper

    Zippo offers some functions to update a zipper.

    The loc-update one takes a location predicate, an update function and the rest arguments. Here is how you douple all the even numbers in a nested vector:

    (let [loc
          (zippo/loc-update
           z
           (zippo/->loc-pred (every-pred int? even?))
           zip/edit * 2)]
    
      (is (= [1 [4 3] [[8]]]
             (zip/root loc))))
    

    For the updating function, one may use zip/append-child to append a child, zip/remove to drop the entire location and so on:

    (let [loc
          (zippo/loc-update
           z
           (fn [loc]
             (-> loc zip/node (= [2 3])))
           zip/append-child
           :A)]
    
      (is (= [1 [2 3 :A] [[4]]]
             (zip/root loc))))
    

    The node-update function is similar but acts on nodes. Instead of loc-pred and loc-fn, it accepts node-pred and node-fn what operate on nodes.

    (let [loc
        (zippo/node-update
         z
         int?
         inc)]
    (is (= [2 [3 4] [[5]]]
           (zip/root loc))))
    

    Slicing a zipper by layers

    Sometimes, you need to slice a zipper on layers. This is what is better seen on a chart:

         +---ROOT---+    ;; layer 1
         |          |
       +-A-+      +-B-+  ;; layer 2
       | | |      | | |
       X Y Z      J H K  ;; layer 3
    
    • Layer 1 is [Root];
    • Layer 1 is [A B];
    • Layer 3 is [X Y Z J H K]

    The loc-layers function takes a location and builds a lazy seq of layers. The first layer is the given location, then its children, the children of children and so on.

    (let [layers
          (zippo/loc-layers z)]
    
      (is (= '(([1 [2 3] [[4]]])
               (1 [2 3] [[4]])
               (2 3 [4])
               (4))
             (for [layer layers]
               (for [loc layer]
                 (zip/node loc))))))
    

    Breadth-first seq of locations

    The clojure.zip package uses depth-first method of traversing a tree. Let’s number the items:

           +-----ROOT[1]----+
           |                |
     +----A[2]---+     +---B[6]--+
     |     |     |     |    |    |
     X[3] Y[4] Z[5]   J[7] H[8] K[9]
    

    This sometimes may end up with an infinity loop when you generate children on the fly.

    The loc-seq-breadth functions offers the opposite way of traversing a zipper:

           +-----ROOT[1]----+
           |                |
     +----A[2]---+     +---B[3]--+
     |     |     |     |    |    |
     X[4] Y[5] Z[6]   J[7] H[8] K[9]
    

    This is useful to solve some special tasks related to zippers.

    Lookups

    When working with zippers, you often need such functionality as “go up/left/right until meet something”. For example, from a given location, go up until a parent has a special attribute. Zippo offers four functions for that, namely lookup-up, lookup-left, lookup-right, and lookup-down. All of them take a location and a predicate:

    (let [loc
          (zip/vector-zip [:a [:b [:c [:d]]] :e])
    
          loc-d
          (zippo/loc-find loc
                          (zippo/->loc-pred
                           (fn [node]
                             (= node :d))))
    
          loc-b
          (zippo/lookup-up loc-d
                           (zippo/->loc-pred
                            (fn [node]
                              (and (vector? node)
                                   (= :b (first node))))))]
    
      (is (= :d (zip/node loc-d)))
    
      (is (= [:b [:c [:d]]] (zip/node loc-b))))
    

    In the example above, first we find the :d location. From there, we go up until we meet [:b [:c [:d]]]. If there is no such a location, the result will be nil.

    Also See

    The code from this library was used for Clojure Zippers manual – the complete guide to zippers in Clojure from the very scratch.

    © 2022 Ivan Grishaev

  • Деформация

    Последний месяц я вожусь с ботами для Телеграма. Занятная вещь, и как-нибудь расскажу об этом подробней. А пока что мелкое наблюдение.

    Чтобы отсечь спамеров, мой бот выводит задачу уровня 3 + 5 и кнопки с ответами. Кнопок шесть: одна правильная, остальные неправильные. Писать текст нельзя, только жать на кнопку. Если ответил верно, ограничения снимаются. После трех ошибок бан.

    Код для генерации кнопок я писал впопыхах. Коротко, он такой: взять пять случайных чисел, добавить к ним ответ, перемешать. Но не учел, что в случайных числах может выпасть ответ, из-за чего будет две правильные кнопки. Это случается редко, но все же. Вот один из случаев:

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

    С одной стороны, что значит “какой ответ правильный”? Если задача 3 + 5 и две кнопки 8, обе из них правильные. Не бывает двух разных цифр 8 — она одна. Поэтому вместо истерики надо жать ту кнопку, которая по душе — ближе, дальше, круглее, — главное, чтобы на ней была восьмерка.

    С другой стороны, айтишников легко понять. Мы привыкли, что за текстом на кнопке стоит айдишник, который уходит на сервер, и все рассчитывается по айдишнику. Вполне возможно, у первой кнопки “8” айди btn_4, а у второй — btn_9. И не докажешь, что не верблюд.

    Помню, меня страшно бесили тесты по английскому на компе. Нужно было перетащить слова в прочерки, при этом глагол is часто встречался дважды. И хотя я все расставлял правильно, получал ошибку. Оказалось, у каждого слова свой айдишник, а в базе данных задана их последовательность. Имеем два is с номерами 2 и 5. Не так расставил — ошибка.

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

    Происходит деформация: даже если на двух кнопках написано одно и то же, мы не верим. Мы ищем подвох, потому что слишком часто были обмануты. В результате две кнопки 8 кажутся такой же ошибкой, как 3 + 5 = 9.

    Сюда же следует отнести штучки JavaScript вроде [1, 2, 3] == [1, 2, 3]. Только что проверил в консоли — вернет ложь. На JavaScript работают миллионы сайтов и устройств, а конца этому не видно. Весь этот бред так и тянется от стандарта к стандарту.

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

    Подобно тому, как нужно не курить, не бухать и заниматься спортом, нужно работать с правильными технологиями. Такими, где массив [1, 2, 3] все-таки равен другому такому же массиву; где нет пяти функций сравнения; где код вернет то, что ожидаешь, а не то, что прописано на сотой странице стандарта. И деформации в голове станет меньше.

  • REPL, Cider, Emacs (часть 2/4)

    Все части

    Оглавление

    REPL в редакторе

    До сих пор мы набирали код в терминале, что не совсем удобно. Терминал подходит для коротких команд, но плохо справляется с многострочным кодом. Будет правильно набрать код в редакторе, а затем скопировать в терминал. Заодно сохраните код в файл, чтобы не набирать его в следующий раз.

    Со временем вы заметите, что переключение между редактором и терминалом отнимает время. Было бы здорово связать редактор с REPL напрямую. Вы набираете код и с помощью комбинации клавиш выполняете его в REPL. В отдельной области редактор показывает результат. С таким подходом нам доступна мощь обеих сред: REPL и редактора.

    Read more →

  • Веб-файлы

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

    Вот скриншоты с официального сайта Дропбокса. Первая картинка:

    Что мы видим? Всюду адская разреженность. Изображение с отступами, сверху и справа два жирных бара. Иконки в барах мелкие и висят в воздухе. Дикая несогласованность элементов: сперва кнопка с надписью, кнопка с многоточием, заголовок, синяя кнопка со стрелкой вниз.

    Что делает заголовок среди кнопок? Там не поместится ничего длиннее пары слов. Почему кнопка Share синяя, в чем ее особый статус? Почему выпадашка слева отмечена многоточием, а у Share стрелка вниз?

    Зачем колокольчик в окне просмотра файла? Это события, связанные с этим файлом или любые события? Зачем сайдбар справа? Нельзя было уместить жалкие три кнопки наверху?

    Другая картинка:

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

    Наверху значок лупы, но поля ввода нет. Полагаю, оно появится по клику на лупу. Зачем кликать, если можно сразу нарисовать поле?

    Зачем земной шар? Открыть какую-то ссылку? Если да, то какую, и зачем мне туда переходить? Почему бы не разместить гиперссылку с текстом, например, “на сайт”?

    Полоса с текстом “Syncing” срезает столько места, что хватило бы на пару файлов.

    Третья картинка, но там уже нечего комментировать: сочетание косяков в разных пропорциях.

    А вот картинка, открытая в Preview на маке:

    • заголовок отдельно от кнопок. Влезет даже предложение.
    • изображение занимает максимальную площадь
    • виджет поиска отрисован с полем ввода
    • все иконки одинаковы и пригнаны друг к другу
    • по желанию в тулбар можно загнать другие иконки и поменять их местами
    • можно сменить показ иконок: только картинка, только текст или вместе.

    Дело даже не в маке: если взять Total Commander или виндовый Explorer, там то же самое: информация показана компактно и удобно. Могут быть придирки к конкретной программе: скажем, Explorer в режиме значков такой себе, но со списком можно жить. Это уже дело вкуса. Важно, что всем файловым менеджерам свойственна компактность и гибкость в настройках.

    А с вебом беда. Мамкины дизайнеры каждый день видят нормальный интерфейс, но рисуют в Фигме говно. Хотя ничего придумывать не надо: повтори Finder или Explorer, и люди скажут спасибо. Но нет: дизайнер дизайнерит.

    Беда не только у Дропбокса. Гугл, Слак и другие тяжеловесы не могут показать файлы в браузере. Каждый раз выходит ужас: растрата места, жирные бары, несогласованные кнопки — тут многоточие, там выпадашка. Иконки либо мелкие, либо гротескно жирные, словно в азбуке для малышей. Ни у одной иконки нет подписи, нужно наводить курсор или жать наудачу.

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

  • REPL, Cider, Emacs (часть 1/4)

    Все части

    Оглавление

    Эта глава расскажет о REPL — фундаментальном свойстве Clojure. Так называют интерактивную работу с языком, когда программу наращивают постепенно. Вы узнаете, что такое REPL-driven development и почему, однажды познав, от него трудно отказаться.

    Аббревиатура REPL происходит от четырех слов: read, eval, print и loop. Дословно они означают прочитать, выполнить, напечатать и повторить. REPL — устойчивый термин, под которым понимают интерактивный режим программы.

    Многие современные языки поддерживают интерактивный режим. Как правило, он запускается, если вызвать интерпретатор без параметров. Например, команды python или node запустят интерактивные сеансы Python и Node.js. В Ruby для этого служит утилита irb (где i означает interactive). REPL поддерживают не только интерпретаторы, но и языки, которые компилируются в байт-код (Java, Scala) или машинный код (Haskell, SBCL).

    Несмотря на это разнообразие, именно в Лисп-системах REPL имеет решающее значение. Если в Python или Node.js его рассматривают как приятное дополнение, то в Лиспе он необходим. Разработка на любом Лиспе зависит от того, насколько хорошо вы взаимодействуете с REPL. На REPL так или иначе опираются все инструменты и практики, примеры из документации.

    В мире Лиспа ходит понятие REPL-driven development. Это стиль разработки, когда код пишут малыми порциями и запускают в REPL. С таким подходом сразу видно поведение программы. Легче проверить неочевидные случаи, например вызвать функцию с nil или обратиться к ресурсу, которого не существует.

    Другой полезный сценарий для REPL — извлечь данные из сети и исследовать их. Эта задача идеально ложиться на интерактивный режим. Как правило, HTTP-запрос предполагает несколько этапов: его подготовку, отправку, чтение тела и поиск нужных полей. Эти шаги проходят интерактивно методом проб и ошибок. Ниже мы рассмотрим пример HTTP-запроса в сеансе REPL.

    Read more →

Страница 2 из 59