• О еврочиновниках

    Я как-то писал о проблеме, что каждый сайт показывает выпадашку с куками. В конце обещал рассказать о еврочиновниках, которые все это устроили. Эта заметка – о них.

    В широком смысле еврочиновник не отличается от российского или американского чиновника. Их главная черта – реактивный стиль управления. “Реактивный” означает не “быстрый”, а от слова “реакция”. Сперва чиновник реагирует самым глупым образом, а потом (и если вообще) думает, нужно ли было реагировать.

    Пример: если с крыши падает лед, поставим таблички “осторожно, падает лед”. Если кто-то следит за пользователем при помощи кук, поставим плашку “этот сайт использует куки”. В обоих случаях главное – выполнить KPI, отчитаться и пойти дальше. О последствиях чиновник не думает, ему не до таких мелочей.

    В результате 99 сайтов из 100 показывают плашки, что сайт исползует куки. Уже одно оформление говорит о полной неразберихе. На одном сайте это полоска внизу экрана, а на другом – модалка во весь экран. Где-то одно предложение, где-то графомания, которая не вмещается по высоте. Кто-то блюрит контент, пока не нажмешь кнопку.

    На фоне неразберихи появляются сервисы, которые берут часть проблем на себя. Например, Cookiehub внедряет виджет выбора кук. Их клиентов можно понять: проще платить двадцать евро в месяц, чем попасть по доносу конкурентов на 30 тысяч евро (реальный случай).

    Грамотность чиновников, которые принимали закон, сводится к урокам информатики в школе. Учитель заставлял их открыть сайт в Интернет-Эксплорере, найти файлы куки, удалить их и убедиться, что сайт не узнает пользователя. Все мы делали это в школе, и наши европейские сверстники не исключение. Беда в том, что в 40 лет их компьютерная грамотность осталась на том же уровне. Отслеживать пользователя можно сотней способов, но у чиновника в голове одно: куки.

    Когда смотришь, во что превратился интернет, разве не очевидно следующее? Если каждый сайт уведомляет о технологии X, это значит, она повсеместна и уведомлять о ней бессмысленно. Бесконечные плашки превращаются в шум, люди закрывают их инстиктивно. Блокировщики рекламы уже включают функцию удаления плашек. На одном конце провода эти плашки продуцируются, на другом – удаляются. Жгутся такты процессора, выделяется тепло. Зачем? Какая цель?

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

    Почему бы не подвергнуть закон пересмотру? Найти чиновников и спросить: что хорошего принесли уведомления о куках? Стало ли меньше трекинга и преступлений? Стал ли интернет безопасней? Было ли хоть раз так, что человек заходит на сайт и такой: опачки, здесь куки, пойду-ка я на другой сайт? Это просто смешно.

    Поэтому я считаю, что любой закон должен приниматься только на определенный срок. Годен до, так сказать. Когда срок истек, чиновники собираются и решают – продлить как есть или что-то поменять. Ключевой момент в том, что это будут уже другие чиновники. Кто-то из прежних перейдет в другие отделы, кто-то в бизнес, женщины – в декрет. Придут новые ребята и, возможно, поправят бред, принятый по ошибке десять лет назад. Это даст шанс, что ошибка продлится только несколько лет, а не сотню-другую.

    Искренне не понимаю, почему в Европе не нашлось энтузиастов, которые положили бы этому конец? Я имею в виду группу экспертов: программистов, опенсорщиков, членов всяких комитетов. Они бы собрали материалы и факты, организовали бы встречу с чиновниками и популярно объяснили им, что они натворили. Добились бы частичной отмены и точных формулировок. Но ничего подобного не видно: все послушно лепят плашки на сайтах, словно овцы, которых ведут на убой. Всякие Греты Тунберг и другие отбитые личности собирают толпы, когда речь о борьбе с климатом и ковиде. А когда что-то важное, то тишина: это для нашей безопасности.

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

  • Аналоговое образование

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

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

    В образовании айтишный элемент является чистым наркотиком. Это бесплатная первая доза, когда платформу внедряют якобы бескорыстно, а потом выясняется, что нужно купить тарифный план. Сайт платформы работает бесплатно только на десктопе. Мобильное приложение показывает только часть данных, а если хочешь все – оформляй подписку.

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

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

    Гаджеты и платформы плохо влияют на учителей, они деградируют в профессиональном плане. Во время ковидной истерии все родители познали дно этой деградации. Ни один учитель не мог достойно организовать удаленное образование. Ни у кого нет материалов для распечатки, контрольных, типовых заданий. Учитель не может составить задание ученикам. Каждое домашнее задание – это ссылка на какую-то говно-платформу, где нужно регистрироваться и подтверждать учетку телефоном, и которая работает только в Хроме на винде.

    Учителя и школы оказались голыми. Нет никакой базы знаний: офлайн-лекций, методичек, контрольных работ, которые распечатал и готово. Я как отец трех детей (двое учатся в школе) просто ох…ал с этого, простите. Когда училка опять кидает ссылку на образовательный портал, хотелось лупить ее линейкой со словами: ты учитель или кто? Сядь и составь задание для детей, это же час работы. Тебе же самой пригодится в дальнейшем. Какого хрена ты тащишь детей в очередной стартап?

    Я вообще считаю, что учитель без личной базы лекций, заданий и методичек – это не учитель. И на проверку большинство учителей оказались такими.

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

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

    Классика: все онлайн-штучки на проверку оказываются эфемерны. Что-то лагает, тут недоступно, сессия истекла, зайдите через сервис Х, браузер не поддерживается, у вас блокировщик рекламы, введите логин, который ребенок придумал год назад… словом, привычное нам айтишное дерьмо. Я стерплю, не вопрос, но причем тут мои дети?

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

  • Что такое Disruptor?

    Графомания бывает не только в русском, но и в английском языке. Хороший пример — страница проекта Disruptor.

    Вижу большой заголовок “What is Disruptor?”, но ни один параграф не отвечает на вопрос, что это такое.

    Первый параграф о том, что в такой-то фирме борются за перформанс.

    Второй параграф о том, что Disruptor — это результат исследований и тестов. Мы тестировали кеши процессора и ядра и сделали дружественный к железу фреймворк.

    Третий параграф — это не специализированное решение, подойдет всем.

    Четвертый параграф — он работает не так как вы привыкли, может быть непривычно.

    Далее ссылки и документация, а еще нас залайкал известный чел.

    Внимание, вопрос — что такое Disruptor?

    Вижу такое постоянно. Автор пишет заголовок, а дальше Остапа понесло: мы работали, исследовали, у фирмы долгая история, а вот был случай…

    Не надо так. Отвечайте на заголовок, который сами же написали.

  • Вырезалки на Маке

    У меня тут накладки: вышел из строя личный ноут. Ставить Телеграм и прочие штучки на рабочий ноут я не рискую, поэтому пару дней потратил вот на что: достал с антресолей стренький мак 2014 года. Мой первый мак, в котором, как в греческом корабле, давно заменены все детали. Но я считаю его тем самым маком.

    Переустановил ось, накатил софт, и в процессе пришла в голову эта заметка.

    На том маке, что сейчас в отключке, я работаю четыре года. Там настроено все: блокировщик рекламы и вырезалка плашек с куками; отключены все нотификации; много правил для фаервола Lulu, чтобы программы не лезли в интернет за обновлениями; Фаерфокс работает с полиси, чтобы не показывать модалки и все остальное.

    В один момент я всего этого лишился. Ощущения ужасные.

    Начнем с того, что каждая программа после запуска хочет показывать нотификации. Неважно какая, неважно зачем. Даже терминал! Уведомления нельзя отключить всем приложениям сразу и позже настроить исключения. Нужно прокликать “нет” для каждой приложеньки.

    Про то, что каждое приложение лезет в сеть за обновлениями и показывает модалку, я писал много раз. Приходиться ставить Lulu и затыкать софт.

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

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

    Каждый сайт встречает плашкой о том, что использует куки. Каждый без исключения. На StackExchage и StackOverflow плашки размером с могильные плиты. Они не умещаются по высоте, нужно скроллить.

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

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

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

    Вырезаю все это по-новой и думаю: что будет через десять лет?

  • Схемы JSON

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

    В результате схема в масштабе 30% процентов выглядит крупнее, чем в 300%. Разница, на минуточку, целый порядок. Такой вот забавный факт.

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

  • Ответ с эмодзи

    Важная новость: в Microsoft Outlook появились реакции к емейлам. Работает так: вы написали письмо, отправили, а под ним появляются пальчики, сердечки и прочее. Пошарить скриншот не могу, поверьте на слово.

    Интересно, как это работает? Особая апишка в протоколе Outlook? Или пустое письмо с заголовком? Или какой-то особый пейлоад? Или если в письме только эмодзи, оно показывается по-другому?

    В любом случае, шлю лучи добра пользователям Аутлука.

  • Корпоративные обновления

    Когда работаешь на корпоративном ноуте, постигаешь всю беспощадность обновлений. Ничего нельзя отключить, в фоне работают программы, которые следят за обновлениями. Раз в несколько дней обновится то Хром, то Idea, и начинаются выпадашки: Иван, чтобы защитить себя от угроз, поставь обновление! Откладывать можно фиксированное число раз, и в какой-то момент обновление ставится силой — конечно, перед релизом или звонком. Поэтому со временем я стал накатывать обновления на выходных.

    Так вот, в который раз попадаюсь на дурацкое поведение Эпла. Вылазит выпадашка: обновись до Секвойи, тянуть больше нельзя. И кнопочка “Install update”. Я закрываю все программы, все реплы, докеры и остаюсь с выпадашкой наедине. Жму кнопку и вижу: отлично, загружаю обновление, осталось 3 часа. Три часа, Карл! Я мог бы ничего не закрывать и работать чуть ли не полдня, пока качается обновление!

    Нажимая “Install update”, я рассчитываю, что оно уже загружено и я в шаге от того, чтобы начать установку. А загрузка даже не начиналась! И качать там не три мегабайта, а 5-7 гигов.

    Спрашивается, что мешало скачать обновление в фоне? Если от него нельзя отделаться, так скачай сам, зачем парить мне мозги?

    Дизайнер тоже хорош: если кнопка запускает загрузку обновления, ее нужно назвать “Download & Install update”. Я ведь хочу самую малость: чтобы на кнопке было написано то, что она делает на самом деле.

    Сюда же относится виджет обновления Эппловских программ. Вылазит окошко поверх всего, и там кнопка “Обновить”. Нажимаешь, оно начинает загрузку, а потом сто раз перехватывает форус. Почему нельзя скачать в фоне? Почему нельзя поставить его в фоне? Зачем тыкать в лицо модалки?

    Та же самое с Саблаймом. Поработав минут 20, он начинает показывать модалку с прогрессбаром: загружаю обновление. Тупица, что мешает скачать обновление в фоне? Зачем ты показываешь прогресс-бар? Чтобы пользователь смотрел на него? Других дел у пользователя нет?

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

  • Taggie

    Taggie is an experimental library trying find an answer for a strange question: is it possible to benefit from Clojure tags and readers, and how?

    Taggie extends printing methods such that types that could not be read from their representation now can be read. A quick example: if you print an atom, you’ll get a weird string:

    (atom 42)
    #<Atom@7fea5978: 42>
    

    Run that string, and REPL won’t understand you:

    #<Atom@7fea5978: 42>
    Syntax error reading source at (REPL:962:5).
    Unreadable form
    

    But with Taggie, it goes this way:

    (atom 42)
    #atom 42 ;; represented with a tag
    

    And vice versa:

    #atom 42 ;; run it in repl
    #atom 42 ;; the result
    

    The value is an atom indeed, you can check it:

    (deref #atom 42)
    42
    

    Tags can be nested. Let’s try some madness:

    (def omg #atom #atom #atom #atom #atom #atom 42)
    
    (println omg)
    #atom #atom #atom #atom #atom #atom 42
    
    @@@@@@omg
    42
    

    But this is not only about atoms! Taggie extends many types, e.g. refs, native Java arrays, File, URI, URL, Date, java.time.* classes, and something else. See the corresponding section below.

    Installation and Usage

    Add this to your project:

    ;; lein
    [com.github.igrishaev/taggie "0.1.0"]
    
    ;; deps
    com.github.igrishaev/taggie {:mvn/version "0.1.0"}
    

    Then import the core namespace:

    (ns com.acme.server
      (:require
        taggie.core))
    

    Now type in the repl any of these:

    #LocalDate "2025-01-01"
    #Instant "2025-01-01T23:59:59Z"
    #File "/path/to/a/file.txt"
    #URL "https://clojure.org"
    #bytes [0x00 0xff]
    #ints [1 2 3]
    #floats [1 2 3]
    #ByteBuffer [0 1 2 3 4]
    ...
    

    Each expression gives an instance of a corresponding type: a LocalDate, an Instane, a File, etc… #bytes, #ints and similar produce native Java arrays.

    You can pass tagged values into functions as usual:

    (deref #atom 42)
    42
    
    (alength #longs [1 2 3])
    3
    

    To observe what happends under the hood, prepend your expression with a backtick:

    `(alength #longs [1 2 3])
    
    (clojure.core/alength (taggie.readers/__reader-longs-edn [1 2 3]))
    

    Internally, all tags expand into an invocation of an EDN reader. Namely, #longs items becomes (taggie.readers/__reader-longs-edn items), and when evaluated, it returs a native array of longs.

    EDN Support

    Taggie provides functions to read and write EDN with tags. They live in the taggie.edn namespace. Use it as follows:

    (def edn-dump
      (taggie.edn/write-string #atom {:test 1
                                      :values #longs [1 2 3]
                                      :created-at #LocalDate "2025-01-01"}))
    
    (println edn-dump)
    
    ;; #atom {:test 1,
    ;;        :values #longs [1, 2, 3],
    ;;        :created-at #LocalDate "2025-01-01"}
    

    It produces a string with custom tags and data being pretty printed. Let’s read it back:

    (taggie.edn/read-string edn-dump)
    
    #atom {:test 1,
           :values #longs [1, 2, 3],
           :created-at #LocalDate "2025-01-01"}
    

    The write function writes EDN into a destination which might be a file path, a file, an output stream, a writer, etc:

    (taggie.edn/write (clojure.java.io/file "data.edn")
                      {:test (atom (ref (atom :secret)))})
    

    The read function reads EDN from any kind of source: a file path, a file, in input stream, a reader, etc. Internally, a source is transformed into the PushbackReader instance:

    (taggie.edn/read (clojure.java.io/file "data.edn"))
    
    {:test #atom #ref #atom :secret}
    

    Both read and read-string accept standard clojure.edn/read options, e.g. :readers, :eof, etc. The :readers map gets merged with a global map of custom tags.

    Motivation

    Aside from jokes, this library might save your day. I often see people dump data into .edn files, and the data has atoms, regular expressions, exceptions, and other unreadable types:

    (spit "data.edn"
          (with-out-str
            (clojure.pprint/pprint
              {:regex #"foobar"
               :atom (atom 42)
               :error (ex-info "boom" {:test 1})})))
    
    (println (slurp "data.edn"))
    
    {:regex #"foobar", :atom #<Atom@4f7aa8aa: 42>, :error #error {
     :cause "boom"
     :data {:test 1}
     :via
     [{:type clojure.lang.ExceptionInfo
       :message "boom"
       :data {:test 1}
       :at [user$eval43373$fn__43374 invoke "form-init6283045849674730121.clj" 2248]}]
     :trace
     [[user$eval43373$fn__43374 invoke "form-init6283045849674730121.clj" 2248]
      [user$eval43373 invokeStatic "form-init6283045849674730121.clj" 2244]
      ;; truncated
      [clojure.lang.AFn run "AFn.java" 22]
      [java.lang.Thread run "Thread.java" 833]]}}
    

    This dump cannot be read back due to:

    1. unknown #"foobar" tag (EDN doesn’t support regex);
    2. broken #<Atom@4f7aa8aa: 42> expression;
    3. unknown #error tag.

    But with Taggie, the same data produces tagged fields that can be read back.

    Supported Types

    In alphabetic order:

    Type Example
    java.nio.ByteBuffer #ByteBuffer [0 1 2]
    java.util.Date #Date "2025-01-06T14:03:23.819Z"
    java.time.Duration #Duration "PT72H"
    java.io.File #File "/path/to/file.txt"
    java.time.Instant #Instant "2025-01-06T14:03:23.819994Z"
    java.time.LocalDate #LocalDate "2034-01-30"
    java.time.LocalDateTime #LocalDateTime "2025-01-08T11:08:13.232516"
    java.time.LocalTime #LocalTime "20:30:56.928424"
    java.time.MonthDay #MonthDay "--02-07"
    java.time.OffsetDateTime #OffsetDateTime "2025-02-07T20:31:22.513785+04:00"
    java.time.OffsetTime #OffsetTime "20:31:39.516036+03:00"
    java.time.Period #Period "P1Y2M3D"
    java.net.URI #URI "foobar://test.com/path?foo=1"
    java.net.URL #URL "https://clojure.org"
    java.time.Year #Year "2025"
    java.time.YearMonth #YearMonth "2025-02"
    java.time.ZoneId #ZoneId "Europe/Paris"
    java.time.ZoneOffset #ZoneOffset "-08:00"
    java.time.ZonedDateTime #ZonedDateTime "2025-02-07T20:32:33.309294+01:00[Europe/Paris]"
    clojure.lang.Atom #atom {:inner 'state}
    boolean[] #booleans [true false]
    byte[] #bytes [1 2 3]
    char[] #chars [\a \b \c]
    double[] #doubles [1.1 2.2 3.3]
    Throwable->map #error <result of Throwable->map> (see below)
    float[] #floats [1.1 2.2 3.3]
    int[] #ints [1 2 3]
    long[] #longs [1 2 3]
    Object[] #objects ["test" :foo 42 #atom false]
    clojure.lang.Ref #ref {:test true}
    java.util.regex.Pattern #regex "vesion: \d+"
    java.sql.Timestamp #sql/Timestamp "2025-01-06T14:03:23.819Z"

    The #error tag is a bit special: it returns a value with no parsing. It prevents an error when reading the result of printing of an exception:

    (println (ex-info "boom" {:test 123}))
    
    #error {
     :cause boom
     :data {:test 123}
     :via
     [{:type clojure.lang.ExceptionInfo
       :message boom
       :data {:test 123}
       :at [taggie.edn$eval9263 invokeStatic form-init2367470449524935680.clj 97]}]
     :trace
     [[taggie.edn$eval9263 invokeStatic form-init2367470449524935680.clj 97]
      [taggie.edn$eval9263 invoke form-init2367470449524935680.clj 97]
      ;; truncated
      [java.lang.Thread run Thread.java 833]]}
    

    When reading such data from EDN with Taggie, you’ll get a regular map.

    Adding Your Types

    Imagine you have a custom type and you want Taggie to hande it:

    (deftype SomeType [a b c])
    
    (def some-type
      (new SomeType (atom :test)
                    (LocalDate/parse "2023-01-03")
                    (long-array [1 2 3])))
    

    To override the way it gets printed, run the defprint macro:

    (taggie.print/defprint SomeType ^SomeType some-type writer
      (let [a (.-a some-type)
            b (.-b some-type)
            c (.-c some-type)]
        (.write writer "#SomeType ")
        (print-method [a b c] writer)))
    

    The first argument is a symbol bound to a class. The second is a symbol bound to the instance of this class (in some cases you’ll need a type hint). The third symbol is bound to the Writer instance. Inside the macro, you .write certain values into the writer. Avobe, we write the leading "#SomeType " string, and a vector of fields a, b and c. Calling print-method guarantees that all nested data will be written with their custom tags.

    Now if you print some-type or dump it into EDN, you’ll get:

    #SomeType [#atom :test #LocalDate "2023-01-03" #longs [1 2 3]]
    

    The opposite step: define readers for SomeType class:

    (taggie.readers/defreader SomeType [vect]
      (let [[a b c] vect]
        (new SomeType a b c)))
    

    It’s quite simple: the vector of fields is already parsed, so you only need to split it and pass fields into the constructor.

    The defreader mutates a global map of EDN readers. When you read an EDN string, the SomeType will be held. But it won’t work in REPL: for example, running #SomeType [...] in REPL will throw an error. The thing is, REPL readers cannot be overriden in runtime.

    But you can declare your own readers: in src directory, create a file called data_readers.clj with a map:

    {SomeType some.namespace/__reader-SomeType-clj}
    

    Restart the REPL, and now the tag will be available.

    As you might have guessed, the defreader macro creates two functions:

    • __reader-<tag>-clj for a REPL reader;
    • __reader-<tag>-edn for an EDN reader.

    Each -clj reader relies on a corresponding -edn reader internally.

    Emacs & Cider caveat: I noticed that M-x cider-ns-refresh command ruins loading REPL tags. After this command being run, any attempt to execute something like #LocalDate "..." ends up with an error saying “unbound function”. Thus, if you use Emacs and Cider, avoid this command.

  • Просто берите Postgres

    Несколько месяцев назад завирусилась статья Just Use Postgres. Она была на всех площадках, а том числе в переводе на русский. Я чуть было не репостнул ее, но передумал. На проверку статья оказалось поверхностной: скажем, автор на полном серьезе сравнивает Postgres с SQLite. Мне показалось, в статье нет глубины, и тезис из заголовка ничем не подтверждается. И хотя вывод верный — Just Use Postgres — автор пришел к нему странным способом.

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

    Последний год я занимаюсь Посгресом все активнее. Я мигрировал большую систему с OpenSearch на Postgres. Это 30 сервисов с большими JSON-документами — от нескольких тысяч до миллионов в каждом сервисе. Нужен поиск по вложенным полям, поиск по вхождению, простое ранжирование, а также всякая отчетность. И пока я все это мигрировал, узнал о Постгресе столько, что хватит на несколько докладов. В том числе — почему именно Постгрес так хорош.

    Я давно понял одну вещь, а миграция только ее укрепила. Хранение данных определяет разработку. Это фундамент, относительно которого планируешь куда копать и что возводить. Кто-то считает, что абстракцией можно уравновесить любое хранилище: сделать так, что get-by-id либо идет в базу, либо качает файл из S3. Это справедливо в простых случаях. На практике каждое хранилище вносит свои особенности, с которыми нужно мириться. Если у вас условные OpenSearch или Cassandra, их особенности будут фонить сквозь код. Избежать этого нельзя. На мой взгляд, Постгрес фонит меньше всех: с ним у вас будет меньше проблем в абстракцях.

    Но довольно расплывчатых слов, перейдем к конкретике. Начну с того, что Постгрес легко ставится и работает на любой машине, будь то локальный комп с Виндой, Маком, Линуксом или сервер. Он есть во всех пакетах. Постгрес написан на Си, и на выходе бинарный файл, которому не нужна Джава. Помню, как ставил Датомик на Убунте — это было тяжело. Вроде бы Джава, “compile once, run everywhere (c)” — но вылетают ошибки о том, что классы не найдены. Оказалось, нужна другая Джава, которую нужно ставить отдельно. С Постгресом такого не было никогда.

    Для Мака есть проект Postgres.app. Это приложение с графическим интерфейсом, чтобы запустить Postgres. Можно скачать любую версию по отдельности; есть убер-приложение со всеми версиями и установленным PostGis. Так что любой человек может завести Postgres + PostGis в два клика.

    Особой похвалы заслуживает докерный образ Postgres. Он очень гибко настраивается: почти любую опцию можно задать переменной среды, легко прокинуть свою конфигурацию. У образа убойная фича: папка, куда можно накидать файлы .sql, .sh и .gz. При запуске образ запустит эти файлы в алфавитном порядке. Если у вас миграции или посев тестовых данных, смонтируйте файлы в образ, и при запуске получится готовая база.

    По наивности я думал, что так работают образы других баз данных. Оказалось нет. Запустил образ Кассандры, а она ничего не знает о первичной настройке. Нужны разделы и таблицы? Создай сам. Нужны топики в Кафке? Создай сам. Нужна точка обмена в RabbitMQ? Создай сам! После запуска образа нужно дождаться, пока поднимутся все потроха (обычно это поллинг порта), а потом создать таблицы и топики. Почему-то в Постгресе подумали над этим, а остальным безразлично. Считаю, что образ postgres нужно брать за образец.

    У документо-ориентированных баз и key-value хранилищ есть преимущество: они хорошо реплицируются в силу дизайна. Часто говорят: вы со своим Постгресом упретесь в потолок, когда нужно хранить данные в разных датацентрах, но при этом иметь легкий доступ из одного центра к другому. Условная Кассандра чувствует себя лучше в подобных условиях. Но забывают, что реплицировать нужно не все данные, а только их часть. Например, только базовые сведения о пользователях и сущностях, чтобы быстро выяснить, в каком дата-центре они лежат и выполнить запрос там.

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

    Postgres тоже не стоит на месте, и в нем все больше средств репликации и кластеризации. Есть проекты вроде Debezium, которые читают журнал WAL и стримят изменения в другие базы, очереди и так далее.

    Postgres силен в проекции данных. Бывает, на одни и те же данные нужно смотреть под разным углом: делать группировки, поворачивать таблицы. Часто нужны левые соединения: это когда слева находится полный набор данных, а справа — неполный, и данные слева не должны пропадать. В редких случаях нужно декартово произведение двух таблиц (каждый по каждому), что тоже делается легко. Есть много функций, которые разбивают данные на строки (переводят массивы в строки элементов) и наоборот — агрегируют записи в коллекции.

    Postgres силен рекурсивными запросами. Это когда запрос разбивается на две части: первичный посев и рекурсивная логика, которая принимает предыдущий результат и порождает новый. Пока он не пустой, строки складываются в итоговую таблицу. За счет рекурсии прекрасно обходятся таблицы со ссылками на себя, например структура папок, иерархия сущностей.

    В финансах очень важны оконные функции: посчитать нарастающий доход, остаток на счете, стоимость проекта по неделям и многое другое. Оконные функции слегка пугают, но достаточно прочитать одну книжку, чтобы овладеть ими (см ниже). Без оконных функций происходит следующее: человек выгребает из базы массив данных и проделывает вручную то, что умеет база — только на порядок медленней и с кучей багов. О похожем случае я как-то уже писал.

    Огромную пользу можно получить из связки materialized view и pg_cron. Напомню, materialized view — это вьюхи, которые сбрасываются в физическую таблицу. На нее можно навесить индексы, чтобы ускорить поиск. Польза таких вьюх огромна — это различные проекции и отчеты. Чтобы каждый раз не гонять огромный запрос, его “запекают” во вьюху и материализуют, после чего выбирают строки обычным SELECT.

    В текущем проекте мы храним огромные JSON-документы. У них сложная структура, но отчетность должна быть плоской. Сначала я писал запросы со множеством операторов #>>, которые извлекают данные из JSON по пути в виде массива. Но со временем стал делать плоские представления этих документов вьюхами — и дело пошло лучше. Аналитикам и менеджерам тоже легче: им постоянно нужные данные, и они пишут запросы сами, чтобы не дергать программистов.

    Расширение pg_cron выводит Постгрес на новый уровень: с его помощью можно выполнить любой скрипт по расписанию. Расширение использует стандартный синтаксис crontab. У меня множество крон-задач на материализацию вьюх и прогрев индексов — их принудительный загон в оперативную память. С pg_cron база становится полностью автономной: не нужен сторонний крон, который пинает скрипты. Я недоволен только одним: pg_cron — стороннее расширение, и его нет в поставке. Однако облачные провайдеры вроде Амазона предустанавливают его.

    На сегодня Постгрес — лучшая база для работы с JSON-документами. Я не в восторге от JSON и насколько возможно, храню данные в таблицах. Но порой выбора нет: бизнес завязан на какие-то стандарты. Пример — медицинский формат FHIR. Это огромные документы тройной и более вложенности. Раскладывать их по таблицам и собирать джоинами тяжело, поэтому их хранят в поле jsonb. У меня похожая ситуация : 30 сервисов, каждый отвечает за свою бизнес-сущность. Это большие JSON-ы, и сервисы гоняют их туда-сюда; на них завязан фронтенд. Я пытался представить их в плоском виде, но это очень трудно.

    Постгрес предлагает богатные возможности для JSON: доступ ко вложенным полям по массиву ключей, обновление вложенных полей, слияние словарей, гибкую замену, индексацию документа целиком или подмножества… Есть даже встроенный язык JSON PATH для поиска! Да, внутри SQL может быть строка с мини-языком JSON PATH. Я использую его в сочетании индексами btree по отдельным полям.

    В последнем Постгресе 17 появилась функция JSON_TABLE. По указанной спеке она переводит JSON в таблицу с выводом типов. Если у вас вектор мап, то легко получить таблицу. JSON_TABLE поддерживает вложенность, в результате чего можно развернуть мапу мап в плоскую таблицу. Далее вы материализуете ее, ставите на крон и готово — можно выбрать плоские данные селектом.

    Для Постгреса создано великое число обучающих материалов: книг, курсов, самоучителей. Многие из них изначально созданы на русском, то есть не являются переводами. Российский вендор Postgres Pro не только пишет отличные книги, но и выкладывает на сайте бесплатно. Бесплатно! Я читал некоторые из них, и это не халтура, а действительно проработанные материалы. Книга Егора Рогова “Postgres изнутри” описывает устройство базы в мельчайших деталях. Наверное, нет книги лучше, чтобы понять, как работают современные базы данных.

    В Постгресе отличный анализатор запросов: он покажет, какие стратегии выбрал движок для обхода таблиц и джоинов; какие индексы были использованы и какую их часть пришлось читать с диска. Да, понадобится время, чтобы понять его вывод. Но иные базы данных не предлагают вообще ничего! Просто дают рекомендации, а дальше пробуй сам. Есть расширения, которые фиксируют медленные запросы и их план выполнения. Расширение pg_stat_statements ведет статистику по всем запросам: число вызовов, частота, минимальное, максимальное, среднее время выполнения, ожидание, объем передачи данных и прочее. Все это помогает отлаживать случаи, когда базе нехорошо.

    В Постгресе достойный полнотекстовый поиск. Для начала подойдет триграммный нечеткий поиск по коэффициенту совпадения. Позже можно добавить ts_vector — вектор лексем и стемминга. Из коробки есть стемминг для десятка языков, в том числе русского. Когда заходят разговоры об OpenSearch и других поисковых движках, оказывается, что на Постгресе можно сделать не хуже и главное — без добавления в систему нового узла.

    У Постгреса обширный тулинг: консольные и графические клиенты — браузерные и настольные. PGAdmin, DBeaver, DataGrip… стандартная утилита psql покрывает множество случаев. Она показывает сведения обо всех сущностях, выводит данные разными способами, умеет импорт-экспорт. Можно редактировать запросы и сущности во внешнем редакторе, например Emacs или Vim.

    Постгрес поддерживает апишку COPY, с помощью которой данные гоняют в обе стороны. Если я хочу слить таблицу в CSV, пишу что-то вроде:

    COPY my_table (id, name, email) to STDOUT
      with (HEADER, format CSV)
    

    и Постгрес выплевывает CSV-шный файл. Можно записать файл на диск или читать построчно из сети. Это работает и в обратную сторону: если я хочу вставить CSV в таблицу, то пишу:

    COPY my_table (id, name, email) from STDIN
      with (HEADER, format CSV)
    

    и стримлю в соединение строки CSV. Кроме CSV, Постгрес поддерживает бинарный формат. Спецификация довольна проста, и на практике он работает на 30% быстрее.

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

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

    Обычно на этом месте расчехляют Питон и всякие FakeMockGenerator-ы — библиотеки для генерации случайных данных по спеке. Этот код долго писать, и вставка тоже будет долгой, потому что мы передаем данные от клиента серверу. Еще нужен рантайм, то есть среда, где запускается код: какая-то машина, нода, плейбук.

    А ведь достаточно написать примерно такой скрипт и выполнить его в PGAdmin:

    insert into documents (id, document)
    select
        gen_random_uuid() as id,
        jsonb_build_object(
            '__generated__', true,
            'meta', jsonb_build_object(
                'eyes', format('color-%s', x)
            ),
            'attrs', jsonb_build_object(
                'email', format('user-%s@test.com', x),
                'name', format('Test Name %s', x)
            ),
            'roles', jsonb_build_array(
                'user',
                'admin'
            )
        ) as document
    from
        generate_series(1, 500) as x
    returning
        id;
    

    Он вставит в базу столько JSON-документов, сколько указано в функции generate_series. Хочешь миллион? Да пожалуйста. Важно, что данные генерятся сразу на сервере — мы не гоняем их по сети. Все происходит внутри Постгреса, и это значительно быстрее. Миллион огромных JSON-ов вставляются за несколько минут. Этот скрипт легко поправить, чтобы поля высчитывались по-другому. Каждый может скопировать его и выполнить на свой базе, не прибегая к Питону.

    Иные жалуются, что SQL устарел. Мол, это архаичный язык, он плохо шаблонизируется, и логично выразить его данными, например JSON-ом. Что тут сказать… Да, SQL со скрипом ложится на всякие ORM. Но сегодня полно библиотек, которые строят SQL из объектов и коллекций. А во-вторых, мне нравится самобытность SQL, то, что он остается вещью в себе. Когда пишешь большие запросы, начинаешь видеть его красоту. Со временем понимаешь, что заменить его нечем — слишком много ситуаций и выражений.

    Я рассматриваю SQL как REPL к данным. Наверное, вы знаете, что программисты на Лиспе днями сидят в репле. О преимуществе REPL-driven develpoment сказано много, и нет смысла повторять. По аналогии, SQL — это репл для данных со всеми преимуществами REPL-driven develpoment. Легко понять, какие данные прилетят в код, выполнив запрос в базе. Вместо быдлокода, который вынимает тысячи записей, исправляет их и записывает в базу, можно выполнить один UPDATE. Поймал себя на том, что целыми днями сижу в PGAdmin, а к коду приступаю в последнюю очередь.

    Повторю тезис, который как-то высказывал. Данные — это отдельный домен. Ключевое свойство домена — его ортогональность другим доменам. Я хочу, чтобы данные не были привязаны ко всяким Питонам и Джавам. Я хочу управлять ими независимо от языка или потребителей. Мне не нравятся встраиваемые хранилища или базы, которые “сливаются” с приложением. Если данные можно извлечь только вызовом метода, я пас.

    Рассказывать о том, как динамично развивается Постгрес, нет смысла. Это видно всем. Есть классический Постгрес, есть вендорский Postgres Pro, где доступны различные фичи до того, как они окажутся в классике — правда, за деньги. Развиваются расширения, появляются новые вендоры, проводят митапы и конференции, выходят книги, видосы… на любой запрос найдется контент.

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

    И вот теперь, в конце шестого листа, я говорю: вот поэтому берите Постгрес.

  • SOLID и контекст

    Когда говорят про SOLID, забывают вот о чем. Принцип SOLID — типичный пример, когда контекст, в котором возникло явление, важнее самого явления.

    SOLID возник в момент, когда в ООП-тусовке царил упадок. До повсеместного перехода на ООП говорили, что объекты решат все проблемы. Достаточно выразить сущности классами и нарисовать UML-схему — и все станет понятно. Звучит примерно как “Земля плоская”, но тогда в это верили. И когда ООП-модель стала буксовать, придумали SOLID, чтобы вдохнуть в нее новую жизнь.

    У SOLID есть даже не аналогия, а буквальный пример из жизни. Каждый тренер знает, что главное у спортсмена — настрой (разумеется, не исключая питание и тренировки). Если настрой в упадке, есть не совсем честные способы его поднять. Скажем, когда команда проигрывает всухую, тренер берет таймаут и выдает “заряженные” клюшки, которыми играли великие спортсмены. Или переставляет участников местами, говоря, что сейчас будем играть по “секретной” тактике. Поскольку спортсмены суеверны, это работает.

    Та же самая история у военных, полицейских, пожарных. У них есть церемонии раздачи “заряженных” девайсов, например, касок погибших героев. Надевая такую каску, боец буквально получает +100 к отваге. Вопрос о том, действительно ли герой носил эту каску, тактично обойдем стороной.

    С принципом SOLID то же самое. Когда стало ясно, что нагромождение классов не решает прошлых проблем, а только добавляет новых, кто-то придумал SOLID. Посыл в том, что отныне мы не блуждаем в потемках, а идем к некой цели. Пишем не просто быдлокод, а по некой методичке. И пусть она спорна и расплывчата, это неважно — есть ориентир. Спортсмен снова мотивирован и готов брать рубежи.

    Поэтому отношение к SOLID у меня спорное. Смысловая составляющая высосана из пальца, но запал колоссальный. Уже десятки лет люди спорят о том, как писать код по SOLID правильно. В этом смысле я снимаю перед создателем шляпу, потому что ведь надо так уметь! — вдохновить толпы народа без какой-либо конкретики.

    Но у любой легенды есть запас прочности, и актуальность SOLID подходит к концу. Я понимаю, когда о нем пишут в рекламных блогах или курсах для новичков. SOLID — это все и ни о чем, универсальный рецепт, из которого можно выжать тысячи текстов. Но удивляет, когда кто-то всерьез рассуждает о том, как в 2025 году писать код по SOLID. Здесь можно сказать одно: как бы ни держалась стюардесса, ее пора закопать.

Страница 4 из 93