• В консоле

    Забавляет, когда пишут “посмотри в консоле”. Получается, что консоль — существительное мужского рода? В пенале, в профиле, в консоле.

    Часто ошибаются с постелью. Я еще в постеле. Опоздал на звонок, потому что в постеле. Опять мужской род.

    Из комментариев: я живу в Рязане. А я в Казане! А я на Кубане!

    Самые невезучие слова — это исключения “бреет” и “стелет”. У рэперов есть выражение “стелить”, то есть удачно складывать рифмы. В комментариях на Ютубе частенько встречается: классно стелишь, бро.

    Интересно, есть ли хоть один рэпер, который пишет “стелешь” правильно? Сомневаюсь.

  • PG2 release 0.1.11: HugSQL support

    The latest 0.1.11 release of PG2 introduces HugSQL support.

    The pg2-hugsql package brings integration with the HugSQL library. It creates functions out from SQL files like HugSQL does but these functions use the PG2 client instead of JDBC. Under the hood, there is a special database adapter as well as a slight override of protocols to make inner HugSQL stuff compatible with PG2.

    Since the package already depends on core HugSQL functionality, there is no need to add the latter to dependencies: having pg2-hugsql by itself will be enough (see Installation).

    Basic Usage

    Let’s go through a short demo. Imagine we have a demo.sql file with the following queries:

    -- :name create-demo-table :!
    create table :i:table (id serial primary key, title text not null);
    
    -- :name insert-into-table :! :n
    insert into :i:table (title) values (:title);
    
    -- :name insert-into-table-returning :<!
    insert into :i:table (title) values (:title) returning *;
    
    -- :name select-from-table :? :*
    select * from :i:table order by id;
    
    -- :name get-by-id :? :1
    select * from :i:table where id = :id limit 1;
    
    -- :name get-by-ids :? :*
    select * from :i:table where id in (:v*:ids) order by id;
    
    -- :name insert-rows :<!
    insert into :i:table (id, title) values :t*:rows returning *;
    
    -- :name update-title-by-id :<!
    update :i:table set title = :title where id = :id returning *;
    
    -- :name delete-from-tablee :n
    delete from :i:table;
    

    Prepare a namespace with all the imports:

    (ns pg.demo
      (:require
       [clojure.java.io :as io]
       [pg.hugsql :as hug]
       [pg.core :as pg]))
    

    To inject functions from the file, pass it into the pg.hugsql/def-db-fns function:

    (hug/def-db-fns (io/file "test/demo.sql"))
    

    It accepts either a string path to a file, a resource, or a File object. Should there were no exceptions, and the file was correct, the current namespace will get new functions declared in the file. Let’s examine them and their metadata:

    create-demo-table
    #function[pg.demo...]
    
    (-> create-demo-table var meta)
    
    {:doc ""
     :command :!
     :result :raw
     :file "test/demo.sql"
     :line 2
     :arglists ([db] [db params] [db params opt])
     :name create-demo-table
     :ns #namespace[pg.demo]}
    

    Each newborn function has at most three bodies:

    • [db]
    • [db params]
    • [db params opt],

    where:

    • db is a source of a connection. It might either a Connection object, a plain Clojure config map, or a Pool object.
    • params is a map of HugSQL parameters like {:id 42};
    • opt is a map of pg/execute parameters that affect processing the current query.

    Now that we have functions, let’s call them. Establish a connection first:

    (def config
      {:host "127.0.0.1"
       :port 10140
       :user "test"
       :password "test"
       :dbname "test"})
    
    (def conn
      (jdbc/get-connection config))
    

    Let’s create a table using the create-demo-table function:

    (def TABLE "demo123")
    
    (create-demo-table conn {:table TABLE})
    {:command "CREATE TABLE"}
    

    Insert something into the table:

    (insert-into-table conn {:table TABLE
                             :title "hello"})
    1
    

    The insert-into-table function has the :n flag in the source SQL file. Thus, it returns the number of rows affected by the command. Above, there was a single record inserted.

    Let’s try an expression that inserts something and returns the data:

    (insert-into-table-returning conn
                                 {:table TABLE
                                  :title "test"})
    [{:title "test", :id 2}]
    

    Now that the table is not empty any longer, let’s select from it:

    (select-from-table conn {:table TABLE})
    
    [{:title "hello", :id 1}
     {:title "test", :id 2}]
    

    The get-by-id shortcut fetches a single row by its primary key. It returs nil for a missing key:

    (get-by-id conn {:table TABLE
                     :id 1})
    {:title "hello", :id 1}
    
    (get-by-id conn {:table TABLE
                     :id 123})
    nil
    

    Its bulk version called get-by-ids relies on the in (:v*:ids) HugSQL syntax. It expands into the following SQL vector: ["... where id in ($1, $2, ... )" 1 2 ...]

    -- :name get-by-ids :? :*
    select * from :i:table where id in (:v*:ids) order by id;
    
    (get-by-ids conn {:table TABLE
                      :ids [1 2 3]})
    
    ;; 3 is missing
    [{:title "hello", :id 1}
     {:title "test", :id 2}]
    

    To insert multiple rows at once, use the :t* syntax which is short for “tuple list”. Such a parameter expects a sequence of sequences:

    -- :name insert-rows :<!
    insert into :i:table (id, title) values :t*:rows returning *;
    
    (insert-rows conn {:table TABLE
                       :rows [[10 "test10"]
                              [11 "test11"]
                              [12 "test12"]]})
    
    [{:title "test10", :id 10}
     {:title "test11", :id 11}
     {:title "test12", :id 12}]
    

    Let’s update a single row by its id:

    (update-title-by-id conn {:table TABLE
                              :id 1
                              :title "NEW TITLE"})
    [{:title "NEW TITLE", :id 1}]
    

    Finally, clean up the table:

    (delete-from-table conn {:table TABLE})
    

    Passing the Source of a Connection

    Above, we’ve been passing a Connection object called conn to all functions. But it can be something else as well: a config map or a pool object. Here is an example with a map:

    (insert-rows {:host "..." :port ... :user "..."}
                 {:table TABLE
                  :rows [[10 "test10"]
                         [11 "test11"]
                         [12 "test12"]]})
    

    Pay attention that, when the first argument is a config map, a Connection object is established from it, and then it gets closed afterward before exiting a function. This might break a pipeline if you rely on a state stored in a connection. A temporary table is a good example. Once you close a connection, all the temporary tables created within this connection get wiped. Thus, if you create a temp table in the first function, and select from it using the second function passing a config map, that won’t work: the second function won’t know anything about that table.

    The first argument might be a Pool instsance as well:

    (pool/with-pool [pool config]
      (let [item1 (get-by-id pool {:table TABLE :id 10})
            item2 (get-by-id pool {:table TABLE :id 11})]
        {:item1 item1
         :item2 item2}))
    
    {:item1 {:title "test10", :id 10},
     :item2 {:title "test11", :id 11}}
    

    When the source a pool, each function call borrows a connection from it and returns it back afterwards. But you cannot be sure that both get-by-id calls share the same connection. A parallel thread may interfere and borrow a connection used in the first get-by-id before the second get-by-id call acquires it. As a result, any pipeline that relies on a shared state across two subsequent function calls might break.

    To ensure the functions share the same connection, use either pg/with-connection or pool/with-connection macros:

    (pool/with-pool [pool config]
      (pool/with-connection [conn pool]
        (pg/with-tx [conn]
          (insert-into-table conn {:table TABLE :title "AAA"})
          (insert-into-table conn {:table TABLE :title "BBB"}))))
    

    Above, there is 100% guarantee that both insert-into-table calls share the same conn object borrowed from the pool. It is also wrapped into transaction which produces the following session:

    BEGIN
    insert into demo123 (title) values ($1);
      parameters: $1 = 'AAA'
    insert into demo123 (title) values ($1);
      parameters: $1 = 'BBB'
    COMMIT
    

    Passing Options

    PG2 supports a lot of options when processing a query. To use them, pass a map into the third parameter of any function. Above, we override a function that processes column names. Let it be not the default keyword but clojure.string/upper-case:

    (get-by-id conn
               {:table TABLE :id 1}
               {:fn-key str/upper-case})
    
    {"TITLE" "AAA", "ID" 1}
    

    If you need such keys everywhere, submitting a map into each call might be inconvenient. The def-db-fns function accepts a map of predefined overrides:

    (hug/def-db-fns
      (io/file "test/demo.sql")
      {:fn-key str/upper-case})
    

    Now, all the generated functions return string column names in upper case by default:

    (get-by-id config
               {:table TABLE :id 1})
    
    {"TITLE" "AAA", "ID" 1}
    

    For more details, refer to the official HugSQL documentation.

  • Preview и текст

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

    и процесс копирования:

    Озарение пришло после того, как я привычно выделил текст, думая, что работаю с PDF. И только потом заметил, что это PNG.

    Распознавание работает неплохо, разве что слетают лидирующие пробелы. Но это ничего: достаточно вставить в редактор, выделить и нажать TAB — и все починится.

    Это еще один довод в пользу Preview. Я уже писал об этой программе и повторю — это произведение искусства. Она умеет невероятно много для работы с картинками и PDF. Доступна из коробки, бесплатна.

    Больше всего я ценю ее за скромность. Preview не требует обновлений, не показывает Tip of the day, не открывает попапы “смотри что я могу”. Она одна стоит того, чтобы купить Мак.

    Возможно, она поможет вам скопировать код из скриншота. Об этом я, кстати, тоже давненько писал: иногда, чтобы месаджер не испортил код, его проще переслать картинкой. А с помощью Preview — восстановить обратно.

    Или скачал ноты для дочки в PDF, а там в подвале реклама. Не беда, накрыл белым прямоугольником — и нет рекламы. Красота же. Где еще так можно?

  • Списки в интерфейсе

    Не понимаю, откуда у дизайнеров такие беды со списками.

    Простое правило: список всегда упорядочен по алфавиту. Всегда и точка. Не по системной айдишке, не по важности, не по фазе Луны, а по алфавиту.

    Если критериев сортировки несколько, список становится таблицей. Клик по колонке переключает сортировку на нее. Но у списка, повторю в третий раз, сортировка одна — по алфавиту. Без учета регистра, конечно.

    На скриншотах видно, что дизайнеры не знают этого правила. Пункт “Turn off…”, хоть и начинается с T, идет первым. Edit message оказался ближе к концу. Refactor — еще до середины, Create gist — предпоследний.

    Дизайнеры объединяют команды в группы, но сами группы идут от балды. В менюшках нет никакой организации. Их можно назвать одним словом — хаос. Каждый раз, когда выпадает такая менюха, как дурак сканируешь с самого начала, вместо того, чтобы прыгнуть на нужное место. O(N) и O(1)? Не слышали.

    То же самое относится к якобы “списку настроек” Эпла. Он выглядит как список, но не ведитесь. Пункты разбиты на группы, между которыми едва заметные разделители. Заголовков у групп нет. Почему Lock Screen, Touch ID и Users в одной группе, а Passwords в другой? Такой вопрос можно задать к любой другой группе.

    Цветовое кодирование сбивает с толку. Сетевые штучки нарисованы синим, а периферия — уши, клава, мышь — белым. Для других групп это правило нарушается: там все цвета. Дурдом.

    Пункт Wifi, хоть и начинается с W, идет первым. Displays — в середине, Battery — ближе к концу.

    Давайте договоримся: любой список идет по алфавиту. Да, пусть Delete окажется наверху, а Translate — внизу. Никто не умрет, и так будет лучше: человек сразу найдет то, что ему нужно.

    Кстати, дурацкие иконки — карандаш, часики, мусорный бак, ножницы, что там еще… — нужно убрать. Никто не ищет Cut по иконке ножниц среди календарей и часов. Дизайнеры-обезьянки качают иконки паками, не понимая, что только мусорят ими.

  • Github IDE

    С тяжелым сердцем смотрю, как Микрософт уродует интерфейс Гитхаба.

    Раньше Гитхаб просто показывал код. Ну, с минимальной подсветкой и ссылками на строки. Все изменилось, как только его купили Микрософты.

    Теперь Гитхаб — что-то вроде онлайн-ИДЕ. Стоит куда-то кликнуть, как появляются попапы, колонки сдвигаются, открываются ссылки для переходов к определению и все остальное. Может быть, кому-то это нужно, но в своем случае не припомню.

    До двойного клика:

    и после него:

    На скриншотах выше — типичный косяк веб-интерфейса. Я всего-то дважды кликнул на функцию read-non-quoted-string, чтобы выделить и скопировать название. В результате дерево каталогов исчезло, код переехал влево, а справа появилась новая выпадашка.

    Разве не уроды? Кто просил двигать колонки и что-то скрывать-открывать? Я просто дважды кликнул.

    Интерфейс словно перешел в режим редактирования, потому что курсор стал вертикальной чертой. При этом текст ввести нельзя — документ по-прежнему read-only. Это просто вынос мозга.

    Обратите внимание, что в результате перестановок курсор оказался в неправильном месте. Я кликнул на середину read-non-quoted-string, а на втором скриншоте он остался на конце сигнатуры за ...in]. При этом выделена правая квадратная скобка. Что происходит?

    Разбудите меня, когда у нас, наконец, будут нормальные дизайнеры.

  • PG2 release 0.1.9: arrays

    The latest 0.1.9 release of PG2 supports Postgres arrays.

    In JDBC, arrays have always been a pain. Every time you’re about to pass an array to the database and read it back, you’ve got to wrap your data in various Java classes, extend protocols, and multimethods. In Postgres, the array type is quite powerful yet underestimated due to poor support of drivers. This is one more reason for running this project: to bring easy access to Postgres arrays.

    PG2 tries its best to provide seamless connection between Clojure vectors and Postgres arrays. When reading an array, you get a Clojure vector. And vice versa: to pass an array object into a query, just submit a vector.

    PG2 supports arrays of any type: not only primitives like numbers and strings but uuid, numeric, timestamp(tz), json(b), and more as well.

    Arrays might have more than one dimension. Nothing prevents you from having a 3D array of integers like cube::int[][][], and it becomes a nested vector when fetched by PG2.

    A technical note: PG2 supports both encoding and decoding of arrays in both text and binary modes.

    Here is a short demo session. Let’s prepare a table with an array of strings:

    (pg/query conn "create table arr_demo_1 (id serial, text_arr text[])")
    

    Insert a simple item:

    (pg/execute conn
                "insert into arr_demo_1 (text_arr) values ($1)"
                {:params [["one" "two" "three"]]})
    

    In arrays, some elements might be NULL:

    (pg/execute conn
                "insert into arr_demo_1 (text_arr) values ($1)"
                {:params [["foo" nil "bar"]]})
    

    Now let’s check what we’ve got so far:

    (pg/query conn "select * from arr_demo_1")
    
    [{:id 1 :text_arr ["one" "two" "three"]}
     {:id 2 :text_arr ["foo" nil "bar"]}]
    

    Postgres supports plenty of operators for arrays. Say, the && one checks if there is at least one common element on both sides. Here is how we find those records that have either “tree”, “four”, or “five”:

    (pg/execute conn
                "select * from arr_demo_1 where text_arr && $1"
                {:params [["three" "four" "five"]]})
    
    [{:text_arr ["one" "two" "three"], :id 1}]
    

    Another useful operator is @> that checks if the left array contains all elements from the right array:

    (pg/execute conn
                "select * from arr_demo_1 where text_arr @> $1"
                {:params [["foo" "bar"]]})
    
    [{:text_arr ["foo" nil "bar"], :id 2}]
    

    Let’s proceed with numeric two-dimensional arrays. They’re widely used in math, statistics, graphics, and similar areas:

    (pg/query conn "create table arr_demo_2 (id serial, matrix bigint[][])")
    

    Here is how you insert a matrix:

    (pg/execute conn
                "insert into arr_demo_2 (matrix) values ($1)"
                {:params [[[[1 2] [3 4] [5 6]]
                           [[6 5] [4 3] [2 1]]]]})
    
    {:inserted 1}
    

    Pay attention: each number can be NULL but you cannot have NULL for an entire sub-array. This will trigger an error response from Postgres.

    Reading the matrix back:

    (pg/query conn "select * from arr_demo_2")
    
    [{:id 1 :matrix [[[1 2] [3 4] [5 6]]
                     [[6 5] [4 3] [2 1]]]}]
    

    A crazy example: let’s have a three dimension array of timestamps with a time zone. No idea how it can be used but still:

    (pg/query conn "create table arr_demo_3 (id serial, matrix timestamp[][][])")
    
    (def -matrix
      [[[[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]
        [[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]
        [[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]]
       [[[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]
        [[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]
        [[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]]
       [[[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]
        [[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]
        [[(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]
         [(OffsetDateTime/now) (OffsetDateTime/now) (OffsetDateTime/now)]]]])
    
    (pg/execute conn
                "insert into arr_demo_3 (matrix) values ($1)"
                {:params [-matrix]})
    

    Now read it back:

    (pg/query conn "select * from arr_demo_3")
    
    [{:matrix
      [... truncated
       [[[#object[java.time.LocalDateTime 0x5ed6e62b "2024-04-01T18:32:48.272169"]
          #object[java.time.LocalDateTime 0xb9d6851 "2024-04-01T18:32:48.272197"]
          #object[java.time.LocalDateTime 0x6e35ed84 "2024-04-01T18:32:48.272207"]]
         ...
         [#object[java.time.LocalDateTime 0x7319d217 "2024-04-01T18:32:48.272236"]
          #object[java.time.LocalDateTime 0x6153154d "2024-04-01T18:32:48.272241"]
          #object[java.time.LocalDateTime 0x2e4ffd44 "2024-04-01T18:32:48.272247"]]]
        ...
        [[#object[java.time.LocalDateTime 0x32c6e526 "2024-04-01T18:32:48.272405"]
          #object[java.time.LocalDateTime 0x496a5bc6 "2024-04-01T18:32:48.272418"]
          #object[java.time.LocalDateTime 0x283531ee "2024-04-01T18:32:48.272426"]]
         ...
         [#object[java.time.LocalDateTime 0x677b3def "2024-04-01T18:32:48.272459"]
          #object[java.time.LocalDateTime 0x46d5039f "2024-04-01T18:32:48.272467"]
          #object[java.time.LocalDateTime 0x3d0b906 "2024-04-01T18:32:48.272475"]]]]],
      :id 1}]
    

    You can have an array of JSON(b) objects, too:

    (pg/query conn "create table arr_demo_4 (id serial, json_arr jsonb[])")
    

    Inserting an array of three maps:

      (pg/execute conn
                  "insert into arr_demo_4 (json_arr) values ($1)"
                  {:params [[{:foo 1} {:bar 2} {:test [1 2 3]}]]})
    

    Elements might be everything that can be JSON-encoded: numbers, strings, boolean, etc. The only tricky case is a vector. To not break the algorithm that traverses the matrix, wrap a vector element with pg/json-wrap:

    (pg/execute conn
                "insert into arr_demo_4 (json_arr) values ($1)"
                {:params [[42 nil {:some "object"} (pg/json-wrap [1 2 3])]]})
    
    ;; Signals that the [1 2 3] is not a nested array but an element.
    

    Now read it back:

    (pg/query conn "select * from arr_demo_4")
    
    [{:id 1, :json_arr [42 nil {:some "object"} [1 2 3]]}]
    
  • Microsoft Teams

    Худшая программа, с которой мне приходится работать — это Microsoft Teams, и вот почему.

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

    Удивляет, что в Микрософте сделали быстрый редактор VS Code, но не осилили месаджер. По-моему, ребят из Teams надо запереть в комнате с командой VS Code, чтобы те передали опыт. Странно, что никому не приходит это в голову.

    За короткую жизнь Teams сменил несколько приложений. Сначала был Teams Classic. Потом вышел Teams for School and Work. Теперь вышел Teams New. Это говорит о том, что первое приложение сделано настолько плохо, что проще выпустить новое, чем исправлять старое. Этого никто не скрывает: первый Teams был сделан тяп-ляп, чтобы не дать Слаке занять весь рынок.

    Read more →

  • Смысл жизни

    Опасаюсь, что после сорока лет я начну искать смысл жизни. Знаете, бывает: живет человек, вроде бы все хорошо, а потом раз — на столе Евангелие, Будда и все такое. Начинаются брожения и поиски бога.

    Но пока мне 38, и вроде бы ничего не предвещает беды. Я по-прежнему уверен, что бога нет, души нет, мир случаен. Вселенная конечна. Любовь — гормоны и инстинкты.

    Философы — балаболы. Ни у одного я не видел крупицы смысла. А если крупицы и есть, то искать их среди 600 страниц — так себе удовольствие.

    У Ницше не понял ни абзаца. Читал Гегеля — ощущения, словно вода сквозь пальцы. Слова понятны, смысла не вижу даже отдаленно.

    Самый клевый чел — Иван Павлов. Он один сделал больше, чем все Гегели, Вольтеры и Канты вместе взятые.

    Читал изыскания Толстого: долгий трактат о том, как он искал бога. Каким-то образом, много раз воздвигнув и разрушив всякие доводы, он построил модель мира, в которой прожил остаток дней. Мне как стороннему наблюдателю это кажется странным. Да, построил свою модель. Это примерно как написать свою версию популярной библиотеки: интересно, стимулирует мозг, не сидишь без дела. Главное — процесс.

    Я не вижу проблем в том, что нет смысла. Зачем смысл? Все, кто утверждали, что нашли его, фактически обманули себя. Подобно Толстому, построили свою модель, которая со стороны смотрится нелепо. Я не хочу жить в модели.

    Нам навязывают смысл еще на этапе сада и школы. И во взрослой жизни найдутся те, кто подскажут: смысл такой-то, нужно делать то и это. А ведь самое лучшее — жить без смысла. Делать то, что хочется прямо сейчас. И не бояться это признать: да, моя жизнь бессмысленна. Но зато интересна!

  • Смысл песен

    Хороши те песни, где кроме хорошей мелодии присутствует смысл. Например, Анна Герман:

    Нужно только выучиться ждать,

    Нужно быть спокойным и упрямым,

    Чтоб порой от жизни получать

    Радости скупые телеграммы.

    В четырех строках — жизнь человека. Другой пример, Pink Floyd, Time:

    Tired of lying in the sunshine, staying home to watch the rain

    You are young and life is long, and there is time to kill today

    And then one day you find ten years have got behind you

    No one told you when to run, you missed the starting gun

    Опять, в четырех строчках — жизнь человека.

    А если взять условный Linkin Park, то их текст окажется набором бессмысленных фраз. Я устал от этой лжи, мне нужны ответы, ты стал тем, к чему я стремился, давление ломает меня и так далее.

    Это не отменяет того, то старые песни у них задорные. Просто любой задор со временем выветривается, а смысл остается.

  • DSL

    У программистов на функциональных языках есть болезнь — выдумывать DSL. Не важно, какую задачу они решают, главное — навертеть что-то похожее на язык и назвать его DSL. Потом поехать на конференцию и выступить, чтобы адепты растаскивали толк по телеграм-каналам.

    Почему-то все забывают, что в аббревиатуре DSL буква D означает домен. Язык, специфичный для своего домена. Вопрос: есть ли у вас домен, под который вы пытаетесь подогнать язык? Как правило, нет.

    Главное свойство домена в том, что он ортогонален другим доменам. Рассмотрим три вещи: Perl, HTML и SQL. Каждая технология занимает свою нишу. Они не пересекаются, а взаимно дополняют друг друга. Поэтому у каждой технологии — свой язык.

    Другой пример: язык команд Redis, XML/XSLT, язык R. Все три — разные сущности, пересечения нет, везде свой язык.

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

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

    Вот почему слово “DSL” пробуждает во мне плохие эмоции. Вам не нужен DSL. Прогуляйтесь до парка, вернитесь и сделайте задачу без DSL.

Страница 6 из 79