• New library: PG.bin

    PG.bin is a library to parse Postgres COPY dumps made in binary format.

    Postgres has a great API to transfer data into and out from a database called COPY. What is special about it is that it supports three different formats: CSV, text and binary. Both CSV and text are trivial: values are passed using their text representation. Only quoting rules and separating characters differ.

    Binary format is special in that direction that values are not text. They’re passed exactly how they’re stored in Postgres. Thus, binary format is more compact: it’s 30% less in size than CSV or text. The same applies to performance: COPY-ing a binary data back and forth takes about 15-25% less time.

    To parse a binary dump, one must know its structure. This is what the library does: it knows how to parse such dumps. It supports most of the built-in Postgres types including JSON(b). The API is simple an extensible.

    Installation

    Add this to your project:

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

    Usage

    Let’s prepare a binary dump as follows:

    create temp table test(
        f_01 int2,
        f_02 int4,
        f_03 int8,
        f_04 boolean,
        f_05 float4,
        f_06 float8,
        f_07 text,
        f_08 varchar(12),
        f_09 time,
        f_10 timetz,
        f_11 date,
        f_12 timestamp,
        f_13 timestamptz,
        f_14 bytea,
        f_15 json,
        f_16 jsonb,
        f_17 uuid,
        f_18 numeric(12,3),
        f_19 text null,
        f_20 decimal
    );
    
    insert into test values (
        1,
        2,
        3,
        true,
        123.456,
        654.321,
        'hello',
        'world',
        '10:42:35',
        '10:42:35+0030',
        '2025-11-30',
        '2025-11-30 10:42:35',
        '2025-11-30 10:42:35.123567+0030',
        '\xDEADBEEF',
        '{"foo": [1, 2, 3, {"kek": [true, false, null]}]}',
        '{"foo": [1, 2, 3, {"kek": [true, false, null]}]}',
        '4bda6037-1c37-4051-9898-13b82f1bd712',
        '123456.123456',
        null,
        '123999.999100500'
    );
    
    \copy test to '/Users/ivan/dump.bin' with (format binary);
    

    Let’s peek what’s inside:

    xxd -d /Users/ivan/dump.bin
    
    00000000: 5047 434f 5059 0aff 0d0a 0000 0000 0000  PGCOPY..........
    00000016: 0000 0000 1400 0000 0200 0100 0000 0400  ................
    00000032: 0000 0200 0000 0800 0000 0000 0000 0300  ................
    00000048: 0000 0101 0000 0004 42f6 e979 0000 0008  ........B..y....
    00000064: 4084 7291 6872 b021 0000 0005 6865 6c6c  @.r.hr.!....hell
    00000080: 6f00 0000 0577 6f72 6c64 0000 0008 0000  o....world......
    00000096: 0008 fa0e 9cc0 0000 000c 0000 0008 fa0e  ................
    00000112: 9cc0 ffff f8f8 0000 0004 0000 24f9 0000  ............$...
    00000128: 0008 0002 e7cc 4a0a fcc0 0000 0008 0002  ......J.........
    00000144: e7cb dec3 0d6f 0000 0004 dead beef 0000  .....o..........
    00000160: 0030 7b22 666f 6f22 3a20 5b31 2c20 322c  .0{"foo": [1, 2,
    00000176: 2033 2c20 7b22 6b65 6b22 3a20 5b74 7275   3, {"kek": [tru
    00000192: 652c 2066 616c 7365 2c20 6e75 6c6c 5d7d  e, false, null]}
    00000208: 5d7d 0000 0031 017b 2266 6f6f 223a 205b  ]}...1.{"foo": [
    00000224: 312c 2032 2c20 332c 207b 226b 656b 223a  1, 2, 3, {"kek":
    00000240: 205b 7472 7565 2c20 6661 6c73 652c 206e   [true, false, n
    00000256: 756c 6c5d 7d5d 7d00 0000 104b da60 371c  ull]}]}....K.`7.
    00000272: 3740 5198 9813 b82f 1bd7 1200 0000 0e00  7@Q..../........
    00000288: 0300 0100 0000 0300 0c0d 8004 ceff ffff  ................
    00000304: ff00 0000 1000 0400 0100 0000 0900 0c0f  ................
    00000320: 9f27 0700 32ff ff                        .'..2..
    

    Now the library comes into play:

    (ns some.ns
      (:require
       [clojure.java.io :as io]
       [pg-bin.core :as copy]
       taggie.core))
    
    (def FIELDS
      [:int2
       :int4
       :int8
       :boolean
       :float4
       :float8
       :text
       :varchar
       :time
       :timetz
       :date
       :timestamp
       :timestamptz
       :bytea
       :json
       :jsonb
       :uuid
       :numeric
       :text
       :decimal])
    
    (copy/parse "/Users/ivan/dump.bin" FIELDS)
    
    [[1
      2
      3
      true
      (float 123.456)
      654.321
      "hello"
      "world"
      #LocalTime "10:42:35"
      #OffsetTime "10:42:35+00:30"
      #LocalDate "2025-11-30"
      #LocalDateTime "2025-11-30T10:42:35"
      #OffsetDateTime "2025-11-30T10:12:35.123567Z"
      (=bytes [-34, -83, -66, -17])
      "{\"foo\": [1, 2, 3, {\"kek\": [true, false, null]}]}"
      "{\"foo\": [1, 2, 3, {\"kek\": [true, false, null]}]}"
      #uuid "4bda6037-1c37-4051-9898-13b82f1bd712"
      123456.123M
      nil
      123999.999100500M]]
    

    Here and below: I use Taggie to render complex values like date & time, byte arrays and so on. Really useful!

    This is what is going on here: we parse a source pointing to a dump using the parse function. A source might be a file, a byte array, an input stream and so on – anything that can be coerced to an input stream using the clojure.java.io/input-stream function.

    Binary files produced by Postgres don’t know their structure. Unfortunately, there is no information about types, only data. One should help the library traverse a binary dump by specifying a vector of types. The FIELDS variable declares the structure of the file. See below what types are supported.

    API

    There are two functions to parse, namely:

    • pg-bin.core/parse accepts any source and returns a vector of parsed lines. This function is eager meaning it consumes the whole source and accumulates lines in a vector.

    • pg-bin.core/parse-seq accepts an InputStream and returns a lazy sequence of parsed lines. It must be called under the with-open macro as follows:

    (with-open [in (io/input-stream "/Users/ivan/dump.bin")]
      (let [lines (copy/parse-seq in FIELDS)]
        (doseq [line lines]
          ...)))
    

    Both functions accept a list of fields as the second argument.

    Skipping fields

    When parsing, it’s likely that you don’t need all fields to be parsed. You may keep only the leading ones:

    (copy/parse DUMP_PATH [:int2 :int4 :int8])
    [[1 2 3]]
    

    To skip fields located in the middle, use either :skip or an underscore:

    (copy/parse DUMP_PATH [:int2 :skip :_ :boolean])
    [[1 true]]
    

    Raw fields

    If, for any reason, you have a type in your dump that the library is not aware about, or you’d like to examine its binary representation, specify :raw or :bytes. Each value will be a byte array then. It’s up to you how to deal with those bytes:

    (copy/parse DUMP_PATH [:raw :raw :bytes])
    [[#bytes [0, 1]
      #bytes [0, 0, 0, 2]
      #bytes [0, 0, 0, 0, 0, 0, 0, 3]]]
    

    Handling JSON

    Postgres is well-known for its vast JSON capabilities, and sometimes tables that we dump have json(b) columns. Above, you saw that by default, they’re parsed as plain strings. This is because there is no a built-in JSON parser in Java and I don’t want to tie this library to a certain JSON implementation.

    But the library provides a number of macros to extend undelrying multi-methods. With a line of code, you can enable parsing json(b) types with Chesire, Jsonista, Clojure.data.json, Charred, and JSam. This is how to do it:

    (ns some.ns
      (:require
       [pg-bin.core :as copy]
       [pg-bin.json :as json]))
    
    (json/set-cheshire keyword) ;; overrides multimethods
    
    (copy/parse DUMP_PATH FIELDS)
    
    [[...
      {:foo [1 2 3 {:kek [true false nil]}]}
      {:foo [1 2 3 {:kek [true false nil]}]}
      ...]]
    

    The set-cheshire macro extends multimethods assuming you have Cheshire installed. Now the parse function, when facing json(b) types, will decode them properly.

    The pg-bin.json namespace provides the following macros:

    • set-string: parse json(b) types as strings again;
    • set-cheshire: parse using Cheshire;
    • set-data-json: parse using clojure.data.json;
    • set-jsonista: parse using Jsonista;
    • set-charred: parse using Charred;
    • set-jsam: parse using JSam.

    All of them accept optional parameters that are passed into the underlying parsing function.

    PG.Bin doesn’t introduce any JSON-related dependencies. Each macro assumes you have added a required library into the classpath.

    Metadata

    Each parsed line tracks its length in bytes, offset from the beginning of a file (or a stream) and a unique index:

    (-> (copy/parse DUMP_PATH FIELDS)
        first
        meta)
    
    #:pg{:length 306, :index 0, :offset 19}
    

    Knowing these values might help reading a dump by chunks.

    Supported types

    • :raw :bytea :bytes for raw access and bytea
    • :skip :_ nil to skip a certain field
    • :uuid to parse UUIDs
    • :int2 :short :smallint :smallserial 2-byte integer (short)
    • :int4 :int :integer :oid :serial 4-byte integer (integer)
    • :int8 :bigint :long :bigserial 8-byte integer (long)
    • :numeric :decimal numeric type (becomes BigDecimal)
    • :float4 :float :real 4-byte float (float)
    • :float8 :double :double-precision 8-byte float (double)
    • :boolean :bool boolean
    • :text :varchar :enum :name :string text values
    • :date becomes java.time.LocalDate
    • :time :time-without-time-zone becomes java.time.LocalTime
    • :timetz :time-with-time-zone becomes java.time.OffsetTime
    • :timestamp :timestamp-without-time-zone becomes java.time.LocalDateTime
    • :timestamptz :timestamp-with-time-zone becomes java.time.OffsetDateTime

    Ping me for more types, if needed.

    On Writing

    At the moment, the library only parses binary dumps. Writing them is possible yet requires extra work. Ping me if you really need writing binary files.

    Scenarios

    Why using this library ever? Imagine you have to fetch a mas-s-s-ive chunk of rows from a database, say 2-3 million to build a report. That might be an issue: you don’t want to saturate memory, neither you want to paginate using LIMIT/OFFSET as it’s slow. A simple solution would be to dump the data you need into a file and process it. You won’t keep the database constantly busy as you’re working with a dump! Here is a small demo:

    (ns some.ns
      (:require
       [pg-bin.core :as copy]
       [pg-bin.json :as json]))
    
    (defn make-copy-manager
      "
      Build an instance of CopyManager from a connection.
      "
      ^CopyManager [^Connection conn]
      (new CopyManager (.unwrap conn BaseConnection)))
    
    (let [conn (jdbc/get-connection data-source)
          mgr (make-copy-manager conn)
          sql "copy table_name(col1, col2...) to stdout with (format binary)"
          ;; you can use a query without parameters as well
          sql "copy (select... from... where...) to stdout with (format binary)"
          ]
      (with-open [out (io/output-stream "/path/to/dump.bin")]
        (.copyOut mgr sql out)))
    
    (with-open [in (io/input-stream "/path/to/dump.bin")]
      (let [lines (copy/parse-seq in [:int2 :text ...])]
        (doseq [line lines]
          ...)))
    

    Above, we dump the data into a file and then process it. There is a way to process lines on the fly using another thread. The second demo:

    (let [conn
          (jdbc/get-connection data-source)
    
          mgr
          (make-copy-manager conn)
    
          sql
          "copy table_name(col1, col2...) to stdout with (format binary)"
    
          in
          (new PipedInputStream)
    
          started? (promise)
    
          fut ;; a future to process the output
          (future
            (with-open [_ in] ;; must close it afterward
              (deliver started? true) ;; must report we have started
              (let [lines (copy/parse-seq in [:int2 :text ...])]
                (doseq [line lines] ;; process on the fly
                  ;; without touching the disk
                  ...))))]
    
      ;; ensure the future has started
      @started?
    
      ;; drain down to the piped output stream
      (with-open [out (new PipedOutputStream in)]
        (.copyOut mgr sql out))
    
      @fut ;; wait for the future to complete
      )
    
  • Место работы

    Я, кстати, придумал вот что: прошелся по всем профилям и убрал текущее место работы. Оставил только vast fintech с описанием технологий — и довольно.

    Почему? Считаю, рекрутерам не нужно знать, где я работаю. Прошлые места смотрите ради бога, а текущее вас не касается. Буду искать работу — созвонимся, и я все расскажу. А пока держите от меня фигу с маслом.

    Есть и моральный аспект. Когда ведешь бложик, работа часто подкидывает темы — в том числе основанные на противоречии, когда не согласен и хочешь выговориться. Чтобы не бросать на кого-то тень, считаю правильным не раскрывать, где работаешь.

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

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

  • Евреи в СССР

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

    Например, идут титры, и написано: композитор А. Шпильман. Взрослые такие с легкой улыбкой – еврей! Как будто раскрыли агента, который тщательно скрывался.

    Я, маленький, долго не понимал, что это за еврей такой и почему он появляется во всех титрах. Я думал, это конкретный человек и удивлялся его продуктивности.

    Помню как прочел фантастический рассказ “Песок” о космонавтах, которые потерпели бедствие на планете-пустыне. Одного из них звали Шапиро. Когда я пересказывал рассказ пожилому родственнику, это звучало так: Шапиро сказал своему другу (“Шапиро? Это еврейская фамилия!”), что они падают, потом Шапиро вышел из корабля (“Шапиро – это еврей!”) и увидел пустыню, затем Шапиро пошел (“Тот самый еврей?”)…

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

    Еще одна кулстори на тему евреев случилась в школе. Урок истории, тема – приход к власти фашистов и преследование евреев. Историк – маленький неженатый мужчина, который делал сомнительные комплименты девочкам и просил их фотографии. И вот он объясняет, за что преследовали евреев: вы понимаете, у них нет своей страны, они везде образуют диаспоры, строят заговоры И ВООБЩЕ — они же Иисуса распяли!

    Как вы думаете, что́ мы, дети, запомнили из этой тирады?

    Следующий урок замещает другая учительница, спрашивает – за что преследовали евреев? Весь класс хором – потому что они Иисуса распяли! У учительницы глаза из орбит, она тихим голосом спрашивает: кто вам это сказал? Отвечаем — Константин Батькович. Она: понятно, я с ним поговорю.

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

  • Настройка uBlock Origin

    Заметка самому себе как настроить uBlock Origin.

    1. На закладке Filters отметить все галочки кроме “Block Outsider Intrusion into LAN”. С ней отваливаются картинки на Ютубе и в других музыкальных сервисах. Почему – пока не разобрался.

    2. Во вкладке My filters добавить такую строку:

    ||accounts.google.com/gsi/*$xhr,script,3p
    

    Она отключает выпадашку с авторизацией Гугла. Эта выпадашка – вселенское зло: закрывается буквально на день и завтра вылезает снова. Ее добавили на все сайты семейства StackExchange. Неважно чем ты интересуешься – базой данных, редактором, версткой – она липнет как навязчивая муха.

    Поражает, сколько фильтров нужно ставить, чтобы просто смотреть интернет. Пользуясь случаем, шлю Гуглу лучи добра за то, что запретили uBlock в последнем Хроме. Я так понимаю, кроме форков Файерфокса вариантов нет?

    Еще одна мысль: обидно, когда в консоли написано “Blocked by uBlock”, но нет информации о том, какой фильтр сработал. Имя фильтра и номер строки были бы спасением. Эти данные по-любому есть в контексте и нужно только добавить их в сообщение. Или я плохо смотрел?

    На закладке Settings включить “Block media elements larger than” и задать свой размер. Полезно, чтобы отключать большие обложки к статьям. Все равно они сгенерены нейронкой.

    Там же: опция “Block remote fonts”. Полезно для сайтов, чьи дизайнеры любят поиграть со шрифтами.

  • Метод Сократа

    В комментариях упомянули диалог методом Сократа. Вспомнил, что хотел написать на эту тему.

    Что такое метод Сократа? Это когда собеседник не возражает напрямую, а задает серию вопросов, чтобы направить другого к истине. “Истина”, “направить” – как благородно звучит!

    К счастью, сегодня есть точное и емкое слово, чтобы описать метод Сократа — это троллинг. В античные времена такого слова не было — как, к примеру, не было термина “пассивная агрессия”. Но явление было, а Сократ стал его мастером.

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

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

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

    Я давно научился узнавать троллей, косящих под Сократа. Высказал какую-то мысль, и подобно мухе к тебе пристал человек. Задает наводящие вопросы: что ты имел в виду, а что ты скажешь на то, на се… Как будто не очевидно: тролль собирает базу высказываний, чтобы сказать: ага, сначала ты заявил X, а потом Y! Ты противоречишь, иди разберись.

    Поэтому если от человека следует поток вопросов, я быстро его прерываю. Либо проходи мимо, либо пиши у себя опровержение. Я даже дам ссылку, мне не жалко.

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

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

  • Оригинальная озвучка (2)

    Как и обещал, пример русской озвучки и оригинала. Предлагаю сцену из первого Гарри Поттера, где он узнает о том, что родственники все от него скрывали. В очередной момент подключается тетя Петуния и дает небольшой монолог. Прослушайте его (ссылки с таймкодами):

    Оригинал:

    Переозвучка:

    Обратите внимание, что в оригинале тетя говорит негромко. Она начинает чуть ли не шепотом от негодования и плавно повышает голос. При этом она не кричит, а больше играет интонацией. И конечно, за счет того, что обе актрисы (на съемках и в озвучке) говорят на одном языке, попадание в губы идеальное. В русской озвучке она говорит громче и с каким-то придыханием.

    Ну и сравните голоса Гарри. В оригинале говорит ребенок ровно того возраста, что и актер в фильме. Это голос мальчика 8-10 лет до пубертата. В русской версии это голос студента театрального училища.

    Когда годами смотришь фильмы в переозвучке, это не парит. Но потом раз — нет-нет да услышишь оригинал, и возникает диссонанс. Почему в оригинале не то что другие голоса, а вообще иные громкость и манера речи?

    Короче, просто посмотрите эти два куска.

  • Оригинальная озвучка (1)

    Чтобы насладиться продуктом в полной мере (игрой, фильмом, книгой), потребляйте его на языке оригинала. Особенно это касается английского контента, и вот почему.

    Я не знаю, как устроена русская озвучка в целом – я имею в виду общие принципы, – но она довольно странная. Там, где в оригинале персонаж шепчет, перводчик орет, словно над ухом. Русская озвучка до неприличия громкая как в маршрутке: громко, четко и заранее.

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

    Послушайте персонажей из “Омерзительной восьмерки” Тарантино. Там каждый голос – праздник. Южный выговор, северный, мексиканский акцент, оттяжки, проглатывание слогов и целых слов. Красота!

    То, что пропадают игра слов и различные отсылки, нет смысла писать. Видимо, на их адаптацию не остается времени.

    Теряются акценты речи. Например, босс называет чернокожего подчиненного “black cunt” – в переводе мы получаем “урод”. Конечно, я бы не хотел слышать дословный перевод, но вместе с тем и не должно быть беззубого сглаживания. Это же бандиты! Персонаж высказался грубо и уничижительно, и нужно как-то это передать.

    Приведу пример, который особенно меня расстроил. В игре Resident Evil 4 Remake есть дополнение за второстепенного персонажа. Это полноценная кампания, которая называется Separate ways – раздельные пути. В русской версии она называется “Два пути”. Казалось бы, довольно близко. Не все ли равно?

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

    I think we both know this is where we go our separate ways.

    Полагаю, мы оба знаем, что теперь наши пути – врозь.

    То есть речь идет о разобщении, о том, что альянсу конец.

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

    Скажите на милость, как русский перевод “Два пути” все это передаст? Ответ – никак, игрок просто проезжает мимо.

    Ясно, что не всегда уровень английского позволяет все понять без пауз и перемоток. Но результат того стоит. То же самое касается книг: в свободное время почитываю Остров сокровищ и Гарри Поттера на английском. Попадаются отличия, но об этом – в другой раз.

  • Гугл и Эпл

    Пишут, что Гугл закручивает гайки: запрещает ставить приложения из .apk-файлов. Основание – борьба с вирусами и обманом пользователей, когда под видом официального приложения им впаривают невесть что.

    Насколько я понял из комментариев, на самом деле будет так: приложения из .apk все-таки можно поставить, но только если они подписаны ключом разработчика. Чтобы получить этот ключ, нужно пройти верификацию Гугла, и, возможно, заплатить (последнее – неточно). Разумеется, если разработчик проштрафится, Гугл отзовет его ключ и все .apk, им подписанные, превратятся в тыкву.

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

    Особым приложениям новое правило навредит. Это некая серая зона: софт, который полезен людям, но не пройдет модерацию Гугла. Например, приложение для диабетиков, которое дает указания по приему инсулина и других лекарств. Рекомендация лекарств запрещена в Гугле, поэтому люди выкладывают самосборный .apk. Что им теперь делать – не знаю.

    Я тоже получил письмо от Гугла с предложением пройти верификацию. Все потому, что лет пятнадцать(!) назад выкладывал в стор приложение – личный кабинет Читинского Энергосбыта. Им даже пользовались, а однажды какая-то УК хотела его купить.

    Вторая новость – Эпл создаст внутренний магазин приложений для России. Там смогут публиковаться те, кто под санкциями: всякие сберы-тиньковы и так далее. Пока что магазина нет, но Эпл сам идет навстречу.

    Я пишу это не для того, чтобы сказать: Гугл плохой, а Эпл – хороший. Дело в другом: принципы айти-гигантов всегда оказываются соглашением в моменте. Многие выбирали Андроид только затем, чтобы ставить ломанные приложухи с 4pda. Яблочников стыдили: у вас рабство. А теперь ситуация если не переменилась, то близка к этому: один гигант закручивает гайки, другой – ослабляет, хотя оба били себя пяткой в грудь и заявляли, что никогда такого не будет.

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

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

  • ИИ-выжимка

    Я часто гуглю и привык, что на первом месте – уже не ссылка или реклама, а блок с выжимкой ИИ (Gemini или как там ее). Должен признать, выжимка довольно точно пересказывает оригинал, и порой ее достаточно. Фактически, гуглить сегодня – все равно что пользоваться GPT: в обоих случаях вам покажут чей-то текст или их комбинацию.

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

    Полезным упражнением может быть вот что. Загуглите что-нибудь, над чем работали только вы. Например, библиотеку на Гитхабе с осмысленным readme. У меня есть такие, где в readme написано много. Скажем, “clоjure github deed” или “clоjure github pg2”. В результате Gemini покажет пересказ твоего же текста.

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

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

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

    Если бы такой кусочек был, они бы увидели, как сильно заблуждаются.

  • A fake Clojure Object equals to what you want

    Imagine you write a unit test where you compare two maps:

    (is (= {:some {:nested {:id ...}}}
           (get-result)))
    

    Turns out, this map has a random UUID deep inside so you cannot blindly compare them with the “equals” function:

    (defn get-result []
      {:some {:nested {:id (random-uuid)}}})
    
    (is (= {:some {:nested {:id ???}}}
           (get-result)))
    

    This won’t work because the nested :id field will is random every time.

    What to do? Most often, people use libraries for fuzzy matching, DSLs, etc. Well, a single case still doesn’t mean you should drag in another library. Apparently, it could be solved with a dummy object that equals to any UUID:

    (def any-uuid
      (reify Object
        (equals [_ other]
          (uuid? other))))
    
    (= any-uuid (random-uuid))
    true
    
    (= any-uuid 42)
    false
    

    Now replace the value in your map, and the test will pass:

    (is (= {:some {:nested {:id any-uuid}}}
           (get-result)))
    

    It works the same for numbers:

    (def any-number
      (reify Object
        (equals [_ other]
          (number? other))))
    
    (= any-number 42)
    true
    
    (= any-number -99)
    true
    

    The only caveat is, this dummy object must be the first one in the = function. It does equal to any object on the left but the opposite is false: a normal UUID doesn’t equal to a fake UUID.

    For the rest, it short and trivial, and no other libraries are needed.

Страница 1 из 100