Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS
📢 Канал в Telegram @stendap_sogodni
🦣 @stendap_sogodni@shevtsov.me в Федиверсі

22.07.2023

Markdown як структурна мова

…Роздивився вихідний код Obsidian Tasks. Зрозумів, чому вони не підтримують вкладені задачі. Виявляється, що вони обрали найпростіший підхід до розбору документів — рядки з задачами шукають регулярними виразами. Ніякого звʼязку між рядками немає, тому немає й вкладеності. За словами самих розробників, без серйозного рефакторингу її не зробити.

При цьому, документ Markdown наділений цілком реальною структурою. Попри відсутність розмітки, а точніше, навіть завдяки їй — бо на відміну від HTML, Markdown дійсно є мовою “what you see is what you get”.

Колись я вже робив менеджер задач на основі Markdown, тільки на React Native. Ідея була в тому, що редагувати текст на телефоні незручно, тому можна перетворити текст на список (в сенсі UI) - наприклад, елементом списку могла бути задача, яку можна було легко закрити, або ж відредагувати текст, пересунути на інше місце, і так далі. Всі ці зміни перетворювались назад у Markdown.

Як це працювало: я брав дерево AST, створене у markdown-it. Далі перетворював це дерево на семантичне, тобто таке, де елементи мають сенс в моєму контексті: параграфи, списки, задачі, і таке інше. Потім це дерево розрізав на елементи списку. Елементи списку вирушали в React Native.

При змінах зі списку наново будується весь документ. Відтворити оригінальний документ теж простіше, ніж з HTML, бо у розмітці Markdown менше варіації. Наприклад, тег HTML може містити довільні пробіли, а у Markdown таких нюансів майже немає — хіба що список можна створити з * або - та інші дрібниці.


21.07.2023

Доповнення Obsidian Tasks

Виявилось, що в Obsidian вже є чарівне доповнення Tasks, яке вирішує 90% моїх потреб. (Більш за те, решту можна доробити, про що пізніше.)

Сам по собі синтаксис задач - [ ] в Obsidian є стандартно. Доповнення Tasks робить дві речі. Додає розширений синтаксис для оздоблення задачі метаданими — це різні дати та пріоритет. Для цього синтаксису є навіть окремий модальний редактор, але ніхто не заважає написати його руками (з автодоповненням!), бо метадані зберігаються у звичайному тексті: - [ ] надіслати звіт 🔺 📅 2023-08-10. Мою ідею про автоматичне занотовування дати початку Tasks вже реалізував.

Та друга функція Tasks - це можливість побудувати список задач з результатів пошуку. Пошук можна робити як по всій колекції, так і по одному документу, а також фільтрувати по статусах, датах, і так далі. Це відразу вирішило мою потребу бачити список поточних задач: path includes next projects / status.type is in_progress / group by filename.

Чого Tasks не вміє категорично, це розрізняти вкладені задачі. Тобто будь-який елемент, розмічений як задача, має рівний порядок. Хотілося б все ж таки щоб в переліку задач не зʼявлялися ті, що мають вкладені. Або — ті, що заблоковані попередніми задачами. Можливо, як раз тут я можу додати функціоналу власним плагіном, якщо буду назначати відповідні статуси. (До речі, окрім класичних “зроблено”/“не зроблено” Tasks дозволяє створювати довільні статуси та багато з них вже мають підтримку тем.)

Стає ясною концептуальна різниця між Obsidian та іншими системами ведення нотаток. Зазвичай системи діляться на “чисто текстові” та “розширені та розумні” - вибір між простотою та користю. Натомість підхід Obsidian - витягати з простого тексту більше структурованої інформації. Це залишає можливість обробляти цей текст потужними засобами — ручними та автоматичними. Наприклад, щоб перетворити свій список задач в правильні анотації Tasks, я просто скопіював його в VSCode та швидко це зробив мультикурсорами.


20.07.2023

Доповнення для Obsidian - як воно все працює

Задався ідеєю зробити доповнення для Obsidian, яке б покращувало роботу з задачами. В цілому, хочеться такий інтерфейс: команди можна писати безпосередньо в файлі, а доповнення при збереженні файлу буде читати команди та заміняти на результати. Поки, щоправда, це дуже абстрактно, але для початку хотілося б щоб до кожної нової задачі приписувалася дата створення.

Зробив маленьке дослідження того, яка модель редактора в Obisidan та як працювало б таке доповнення. Для цього є документація. Також як приклад, є плагін з Prettier та лінтер.

Цікаво, що вони по-різному підходять до заміни тексту. Обидва плагіни спочатку генерують змінений текст. Але Prettier просто заміняє весь зміст редактору, а Linter будує стислий пакет змін бібліотекою diff-match-patch, а потім вже застосовує кожну зміну окремо. Треба ще перевірити, чому, але можливо, що це краще зберігає історію змін.

Також треба знати, що редактор в Obsidian насправді не один, а багато, бо область редагування можна ділити. Там ціла деревоподібна структура, листям якої є конкретні редактори. Але при цьому можна знайти поточний… якщо він є та містить Markdown.

Ще цікава можливість доповнень Obsidian - це додавати в документ власні анотації та й взагалі будь-яку розмітку. Наприклад, задачі можна розфарбовувати за віком.

До речі, для роботи з Markdown як зі структурним документом можу порадити бібліотеку markdown-it. Я раніше використав її, щоб створити структурний редактор Markdown для React Native.


19.07.2023

Приховані проблеми з 64-бітними ідентифікаторами

Коли я рекомендував використати 64-бітні числа як ідентифікатори, я упустив важливий нюанс, а саме те, що не всі мови та середовища їх підтримують. Навіть в наш час, коли майже кожний компʼютер має 64-бітний процесор — від смартфона до сервера.

Ви маєте ретельно перевірити, що ID не проходять в числовій формі там, де мова не підтримує 64-бітні цілі числа. Я впевнений, що такі місця у вас є, принаймні тому, що JavaScript їх не підтримує. Але перевіряти треба все, бо як я писав, наші проблеми почалися не з JavaScript, а з закритої цільової мови. Щоб не створювати додаткових ризиків, краще не тільки перетворювати ці ID на рядки, а й робити їх очевидно не чисельного вигляду — хоча б додати префікс.

При чому, в типовій ситуації надто велике значення ID не призведе до винятку. ID залишиться числом — цілим числом — він тільки округлиться до іншого значення. Абсолютно непомітно.

Поламані ID - це не так погано на фронтенді, бо там вони призведуть хіба що до тимчасової втрати доступу користувача до своїх ресурсів. Але всередині системи це вкрай ризикова помилка, бо ID зазвичай нема чим перевірити, та якщо вони потрапляють в базу, відновити дійсну картину буде складно.

Та ще загальна рекомендація — якщо аналітики знайшли вам баг в даних за допомогою деякого SQL запиту, треба перетворити цей запит на постійний моніторинг. Добре мати набір SQL-інваріантів, які помітять розбіжності до того, як це зроблять аналітики (або гірше, користувачі.)


18.07.2023

Сучасна сцена модів на Doom та GZdoom

Якщо хочеться класної ретро-стрілялки (жанр, також відомий як boomer shooter), варто знати, що моди для Doom роблять й по цей час та вони під час вражають якістю, яку не очікуєш від безплатного проєкту. Треба зазначити, що сучасні рушії для Doom - з яких перший GZDoom - вийшли далеко за межі оригінального рушія — але все ж таки загальне відчуття залишається.

Що відрізняє покоління Doom від інших ігор? Рівні Doom були побудовані з кімнат, кожна з яких є призмою — в основі її лежить довільний багатокутник, а підлога та стеля мають фіксовану висоту. У 1993 році це уможливлювало алгоритм, що сканував екран за стовпчиками та визначав, на яку кімнату та стінку припадає кожний стовпчик. Оскільки вся підлога є пласкою, а стіни — вертикальними, то це потребувало тільки обчислень в площині, бо для тривимірних обчислень потрібна матрична математика, а тогочасні компʼютери були не настільки розумні. (Чи знаєте ви, що Doom вийшов навіть на приставку Super Nintendo?)

А у 2023 цей архаїчний формат рівнів створює цікаве творче обмеження. Та й до того ж моделювати рівні у 2D простіше. Окрім звичайних кімнат, Doom дозволяє створювати всілякі обʼєкти за допомогою відʼємного простору. Наприклад, “кімнатою” може стати ліжко. Або подушка на ліжку. Або навіть телефон на подушці. Головне, щоб це був багатокутник, та над ним нічого, крім стелі, не було. Тож сучасні рівні для Doom це особливий жанр 3D мистецтва.

Для ознайомлення можу запропонувати два різних проєкти. Мод Ashes 2063 являє собою цілий епізод захопливих пригод в постапокаліптичному місті. Деталізація рівнів неймовірна, та особливо цікаво, що вони відтворюють справжні локації — вулицю, заправку, ресторан. Мод MyHouse - це експеримент, який робить з рушія неможливе… сказати більше буде спойлером.

До речі, GZDoom чудово почуває себе на macOS. Втім, багато модів розповсюджуються як “standalone executable” - звісно, тільки для Windows. Тут можна не зволікати, знайти в архіві мода найбільший файл .pk3 та запускати його в власному GZDoom.


17.07.2023

Контексти в Golang - простота

Минулого раз я писав, що контексти в Go існують для зупинки процесу ззовні. С тих пір я намагався вписати їх у свій код, та набув додаткового розуміння.

Розуміння таке: так само як і винятки, контексти не потребують багато уваги, насправді. Не потрібно їх постійно створювати або перевіряти.

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

В інших випадках контекст просто проходить по коду зверху вниз. Що, може, не так й зручно, але тут ще нюанс: контексти потрібні здебільшого там, де відбувається зовнішня комунікація — оскільки саме вона створює неочікувані затримки. В простій бізнес-логіці контексти не потрібні. Хіба що можу уявити, якщо є код, що робить довге обчислення — наприклад, відеокодек — то в ньому має сенс перевіряти стан контексту. (Що, до речі, робиться через конструкцію if c.Err() != nil { return c.Err() }.)

Нарешті, чи потрібно перевіряти, чи повернула функція помилку context.Canceled? Насправді ні — таку помилку можна передавати нагору та само, як і будь-яку іншу. Це виконує задачу контексту: код буде зупинений. Тільки на найвищому рівні — там, де контекст був створений — треба перевірити, що саме трапилось — скасування контексту або інша помилка. ]


16.07.2023

Ruby у VSCode - станом на 2023

Сьогодні приділив уваги своєму оточенню розробки на Ruby в VS Code. Сприяло цьому те, що після увімкнення доповнення Error Lens помилки Rubocop на одному з маленьких проєктів стали більш очевидними та навіть нестерпними. (Rubocop в мене був увімкнений для всіх проєктів, але налаштований — не для всіх.) Окрім банального створення .rubocop.yml та вимикання деяких непотрібних правил, захотілося також, щоб автоматичне виправлення відбувалось після зберігання файлу.

Дізнався, що канонічне доповнення vscode-ruby офіційно оголошене застарілим. Вони рекомендують перейти на доповнення Ruby LSP. Мотивація в тому, що Ruby LSP являє собою мовний сервер, написаний на самому Ruby, що є більш перспективною архітектурою. Та дійсно, хоч обидва доповнення вміють запускати Rubocop на збереження файлу, Ruby LSP робить це помітно швидше — практично миттєво. От тільки дратує те, що обидва доповнення не дозволяють обрати рівень виправлень, хоча це робиться простою зміною аргументу для Rubocop. Тут обговорення.

Поки з всім цим розбирався, зрозумів, що насправді я хочу Prettier. Тим паче що з Ruby LSP можна виправляти складніші помилки Rubocop через вікно “Quick Fix” (cmd+.) А поки намагався налаштувати prettier-ruby, зрозумів, що це просто обгортка над гемом syntax_tree - причому Ruby LSP вже вміє форматувати саме цим гемом. Так що Ruby LSP вже пропонує найкращий автоформатувальник коду для Ruby. Чи добре він працює, зможу поділитися пізніше.

…Але є нюанс. Та дуже важливий. На поточний момент, Ruby LSP не показує в коді синтаксичні помилки. Звісно, це вкрай незручно. Тому після деяких вагань я повернув також доповнення vscode-ruby, тільки увімкнув в ньому єдину функцію Lint.


15.07.2023

Rails на AWS Lambda - чому ні? або чому так?

Поставили питання — чи має сенс розгортати типовий вебдодаток на манеру CRUD на AWS Lambda? Моя перша реакція — а навіщо? Втім, особистого досвіду на це немає, тому спробую розібратись.

З технічної сторони різниці небагато. Як Lambda, так і Fargate, а також Fly.io та інші контейнеризовані хостинги використовують технологію віртуалізації Firecracker. Тому Lambda здатна запустити те ж саме, що й звичайний Docker-хостинг. Відмінність цих сервісів не в реалізації, а в рівні абстракції, яку вони пропонують.

Типовий вебдодаток вже є колекцією функцій, які ми викликаємо за протоколом HTTP. Є тільки один нюанс — в додатку не повинно бути власного стану. Проте сучасна парадигма 12-Factor App пропонує це для всіх додатків. Звісно, як я писав, не всі додатки підходять під цю модель — але типовий CRUD-додаток - цілком.

Деякий “стан” все одно буде в додатку. Це завантажений в оперативну памʼять код, підключення до бази даних, кеш. Все це ми втрачаємо з Lambda - наприклад, код доведеться щоразу завантажувати наново. Звісно, якщо запити робляться часто, то Lambda зі своїм станом буде збережена в памʼяті. Але тоді вона мало чим відрізняється від звичайного сервісу — окрім ціни, звісно.

Оскільки вартість Lambda вимірюється в запитах та їх тривалості, а вартість Fargate - в обсягах, можна обчислити, скільки разів можна запустити Lambda, щоб це було вигідно. Година Fargate з 1 CPU + 2 GB = $0.04947. Мільйон запусків Lambda з 2 GB, кожний з яких триває 100 мс (типовий час для вебдодатка) = $3.53. Виходить, 14000 разів на годину. Не так вже й мало! Виходить, на AWS Lambda можна економічно розгортати сервіси з невеликим навантаженням.

З боку розробки, в деяких моментах Lambda додають складнощів (наприклад, їх не так прямо можна запустити локально), але суттєвої різниці немає. Такі бібліотеки, як Lamby дозволяють прямо використати Ruby on Rails на Lambda, або Jets - пропонує схожу на Rails абстракцію контролерів.

Lambda мають версії, та їх підтримує CodeDeploy. Тому обережне розгортування Lambda можливе, як і екстрене повернення на попередню версію — якщо перед Lambda стоїть обгортка, яка обирає активну версію. Підхід ECS мені більше подобається, бо там сервіс знає, яку версію запускати.

Lambda можна відвантажувати у формі архіву, а не тільки образу Docker. Це суттєво спрощує підготовку додатка, якщо середовище дозволяє. А з іншого боку, збірка Docker універсальна, а з архівом треба розбиратись.

Як з Fargate, так і з Lambda, доведеться створити багато різних ресурсів, щоб сервіс почав працювати — ролі, ALB або Gateway, і таке інше. Простіше буде на Fly.io.

До речі, на Fly.io хостинг в 4 рази дешевше, ніж Fargate, тож математика зсувається. Як я писав, Fly.io варто мати у своєму інструментарії для простого запуску сервісів. Але як я також писав, сервіс не завжди потрібний, а невеликий парк функцій буде більш економічним в підтримці та розміщенні.


14.07.2023

Як перетворити прототип на пул-реквести

Мабуть, всім знайомий підхід до розробки Red-Green-Refactor, де функціонал зʼявляється в коді поступово, та після кожної зміни код завжди працює — єдине питання, що саме він вміє зараз. Одна з переваг такого підходу — це зручні, послідовні пачки змін, які легко зрозуміти вашим оглядачам.

Проте в моїй практиці постійно трапляються задачі, які починаються з прототипа. Прототип зазвичай робиться без чіткого плану змін, бо замість плану відбувається ітеративний процес дослідження та доробки коду, без уявлення, що саме за код доведеться писати та де. В результаті, коли все запрацює, виходить добрячий шмат коду, який в один Pull Request вже не влізе. Головним чином, тому, що зрозуміти його за один підхід неможливо. Що я роблю в таких ситуаціях?

В теорії, можна прототип викинути та зробити наново за планом. На практиці, таке трапляється тільки коли прототип за природою відрізняється від остаточного коду (може, написаний іншою мовою.) А в типовому випадку я доробляю та рефакторю прототип, поки він не досягне стандартів якості стабільного коду. Також на цьому етапі пишуться тести. Все це добре, тільки від того прототип стає ще більше та ще неосяжніше.

Тоді я починаю відокремлювати найпростіші та найізольованіші модулі та робити з них пул-реквести. Недолік: без контексту вони можуть потребувати додаткових пояснень. Перевага: кожен модуль легше зрозуміти сам по собі. До того ж такі модулі не створюватимуть ризиків, що в продукті є напівробоча частина. Останнім пул-реквестом впроваджується найвищий рівень коду, який повʼязує всі інші.

Такий підхід уникає створення тимчасових затичок замість ще не реалізованого функціоналу — бо фактично він вже весь реалізований, та затички будуть зайвою витратою. Як приклад: якщо прототип складається з читання даних та їх обробки, я спершу зроблю ПР для модуля, що обробляє дані, а вже потім для верхньорівневої інтеграційної частини.


13.07.2023

Батчинг в Кафці та інші подробиці

Сьогодні працював над споживачем даних з Kafka та особливої уваги потребував батчинг — тобто збір вхідних повідомлень в пачки такого розміру, який є економічним для подальшої обробки. Батчинг — взагалі складна справа, бо треба не тільки набирати пачку, але й стежити за таймером. А ще думати про безпечну зупинку всього цього сервісу.

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

До речі, щодо клієнтів для Golang. Їх є немало, але знайти хороший не так легко. Баланс проходить як раз за віссю “проста абстракція — відповідність архітектурі Kafka”