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

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

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

25.11.2025

Дослідження чужого проєкту за допомогою LLM

Хочу особливо поділитися технікою, яка мене врятувала під час впровадження SQLiteData.swift. Річ у тім, що ця бібліотека не має стільки документації, скільки умовний ActiveRecord, та як виходиш за межі прикладів, то на пошук в інтернеті можна не розраховувати.

Та ось моя ідея: я склонував собі репозиторій, відкрив в Курсорі, та почав ставити питання. Ось кілька прикладів.

find examples of @fetchone or @fetchall used with parameters passed from parent components

Типовий випадок, як-от ProjectView(project: project). Як мені завантажити дії цього проєкту? Виявилося, що такі запити створюються в ініціалізаторі: _actions = FetchAll(Action.where { $0.id =project.id}).

how do you use toggle()

В документації все сумно. А з коду стає зрозуміло, що Action.update { $0.isCompleted.toggle() }

is there a way to extract statements that become `@FetchOne` and `@FetchAll`  into a separate module? i'm specifically struggling with types

Хотілося зробити DRY, але ніяк не міг зрозуміти, якого ж типу повинна бути функція, яка повертає той запит. (Не конкретний тип, який традиційно довжелезний та заплутаний, а узагальнений.) Тут ШІ мені згенерував цілий туторіал в Markdown, а тип виявився на кшталт some StructuredQueriesCore.Statement>Int>.

how can i get to the OpaquePointer db pointer needed for SQLIte3 calls from my database instance

Оце останнє на сьогодні, бо я хочу додати пошук із UNICODE_NOCASE, а розвʼязок знайшов тільки через команди бібліотеки SQLite3. А як добратися до цієї низькорівневої бібліотеки? Виявилося, дуже легко - через database.write { db in db.sqliteConnection }. Але в документації про це ані згадки.

Якщо розвинути ідею, можна було б відразу всі залежності проєкту таким чином додавати через MCP чи щось таке. Але я на таке поки не спроможний.


24.11.2025

Деякі обовʼязки лід-інженера

Що забув?


21.11.2025

Hario Largo - найкращий заварник для чаю

Хтось любить каву, хтось любить чай, дехто не пʼє ані того, ані того, втім є одна правда: способів заварити каву значно більше, і пристроїв більше, і технік, і місць, де люди піклуються про якість кави. Чаю в цьому сенсі поки щастить менше.

(Взагалі, може здаватися, що це завжди так було, але ні — вся сучасна культура обміркованого споживання кави існує трохи понад 20 років. Але зараз не про те.)

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

Отже, щоб не відтягувати далі: я знайшов заварник Hario Largo. Ідея проста: це колба, в якій вільно плавають чаїнки, а коли сплив час заварювання — чай зливається через клапан (та велике сито) в підготований глечик, чайник чи чашку.

З таким заварником тривіально витримати всі вимоги заварювання. Хочеш промити чай — та без проблем. Хочеш взагалі проливний, як пуер чи улун — це теж можна. Оці чайні бутони, які красиво розкриваються в чайнику — будуть всі на виду. А можна поставити настоюватись травʼяний чай на пʼятнадцять хвилин, а потім швиденько злити. Можна заварити одну чашку, а можна на цілий термос.

Мені також дуже важливо, що тут немає пластику — сама колба скляна, фільтр сталевий, основа гумова. Все розбирається та миється. Підставку, яка продається разом, я використовую і для пуровера.

Та от повірте, між просто чаєм та таким, що правильно заварений — океан різниці.


20.11.2025

Вайб-кодінг — для всього неважливого

Потрапив на очі гостренький заголовок: Лінус Торвальдс вважає що вайб-кодінг це добре, поки не використовується для чогось важливого. Звучить як відмова від всієї ідеї — окрім як в контексті навчання.

Проте заголовок відірваний від контексту (можеш послухати саме інтервʼю, чом би й ні.) Але на мою думку, в цьому реченні є інший зміст — навмисний чи ні, я не знаю. Ми всі пишемо нескінченні гори неважливого коду.

Як тільки це збагнеш, програмування з допомогою LLM розкриється новими кольорами. Ти починаєш дивитися на майбутній код критично: чи це такий код, про який я хочу думати? То буду думати. А якщо код нудний та тривіальний — то нехай ШІ його згенерує.

І такого коду дуже багато. Взагалі виявляється, що важливі, архітектурні рішення робляться на вищому рівні, або принаймні можна винести свої міркування на цей вищий рівень, а потім доручати ШІ заповнити пробіли.

А до того є одноразові скрипти та команди. Або задачі, які можна розвʼязати одноразовим скриптом, але оскільки його надто довго писати, ти цього не робив. Та навіть досліджувати проєкти значно легше, коли кожну складнющу команду git тощо може згенерувати ШІ.

Програмісти взагалі занадто привʼязують власну цінність до коду, що повертає нас до того, що варто памʼятати, що ти інженер.


19.11.2025

Елементи безпеки в дизайні мови Swift

Не знаю достатньо про Rust, щоб прокоментувати насущне фіаско з unwrap(). Скажу тільки, що, як на мене, небезпечно називати метод, який може панікнути, такою невинною назвою.

А розповім краще про дизайн Swift - мови, яка дуже схожа на Rust, насправді. (Ви взагалі знаєте, що на Swift можна писати й серверні застосунки? Ось, наприклад, Vapor, а до нього є ORM, Postgres та все як ми звикли.)

Отже. У Swift навколо значення типу Optional. Просто повсюди! Якщо порівняти із, наприклад, Go, то в Go теж навколо “опціональні” значення, тільки перевіряти ми їх будемо неявно (як-от, якщо функція foo() (*bar, error) повернула помилку, то в *bar порожньо, а якщо ні, то щось буде, та на nil можна окремо не перевіряти.) Проґавили — отримали паніку. Ну так Go не називає себе безпечною мовою, там естетика професійного інструменту, до якого ти мусиш вивчити техніку безпеки.

У Swift, якщо лінь танцювати весь танець розгортування Optional, можемо виконати примусове розгортання: bar!. Воно викличе помилку, якщо значення немає. Але дуже важливо, що така форма візуально підкреслює наш намір. А намір тут — не “довірся мені, я знаю що роблю”, а “я відкидаю безпеку! Стережися!” І це дуже важливий елемент дизайну безпечної мови.

Так само у Swift кожна функція, де може бути викликана помилка, повинна бути підписана словом throws, а її виклик - try. Слово try дозволено тільки всередині блоку do ... catch, або всередині функції, яка сама буде throws. Або — сміливий варіант - try! - каже, що нам байдуже на те, що помилка стане критичною.

Не існує абсолютно безпечних мов. Та те, як мова поводиться у небезпечних ситуаціях, і визначає, наскільки ми їй можемо довіряти.


18.11.2025

Ідіосинкразії SwiftUI

Я дуже люблю React за його передбачуваність. Якщо створити компонент-функцію, вся вона буде виконана. Хуки теж мають передбачувані моменти виклику. React не ідеальний, але ось ця прозорість точно його сильна сторона.

У SwiftUI прозорості немає. Життєвий цикл компоненти заплутаний. Хочу я робити запит, який приймає в себе аргумент компоненти, умовно Action.where { $0.contextID == contextID }. Цілком нормальна справа. Написати це прямо в анотації @FetchAll, як каже документація — не вийде. Бо анотації не мають доступу до аргументів — хоч цей запит наче буде виконаний вже в конкретному екземплярі.

Тоді є документація про динамічні запити… це трохи не те, але працювати буде. Воно використовує метод .task. І це ще треба знати — я сьогодні дізнався — що в нього є аргумент id, який насправді працює так само як масив залежностей у React.useEffect. Тобто буде ще й оновлювати запит, якщо цей id зміниться.

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

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

Тільки й це не все розвʼязує, бо ініціалізатор не має доступу до обʼєктів @Environment (це як контексти в React.) Так що якщо ти раптом береш значення звідти — доведеться робити це в батьківській компоненті та передавати аргументами туди, де збираєшся робити запити.

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

Також додам, що XCode це єдине, що дійсно змушує мене замислитись про перехід з MacBook Air до чогось… важчого. А мені не хочеться.


17.11.2025

З JSON до SQLiteData.swift

Вже понад рік я користуюся власним застосунком для GTD. Це, безумовно, успіх — в мене таке може вперше, щоб система так довго не розвалювалася. Але є проблема: він зберігає дані в єдиному файлі JSON.

Мало того, що такого рішення недостатньо, щоб його поширювати, так і мені самому в JSON почало бути тісно. Кожна зміна, навіть така мінімальна як редагування тексту задачі, призводила до збереження всього стану, а потім — як я розумію — широкого перемальовування застосунку. Гальмувати стало ну просто неприйнятно довго.

Отже, на вихідних взявся за переписування всієї моделі даних на повноцінну базу даних. Минулого року я залучив GRDB, а цього разу хотілося випробувати SQLiteData. Ця бібліотека, власне, є надбудовою для GRDB, а відрізняє її можливість синхронізувати базу за CloudKit - тобто забриніла можливість зробити й версію для iOS, що нарешті зробило б цей проєкт продуктом.

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

Головних перепон тут було дві. Перша — вони використовують багато макросів (для побудови SQL з Swift.) Через них компілятор часто не міг дати нормальну помилку та доводилося її шукати. Друга — з мало відомою бібліотекою болюче не вистачає документації та взагалі пояснень. Та ШІ в таких умовах теж мало допоможе!

Тому доводилося читати код самої бібліотеки та шукати там відповіді навіть на прості нібито питання. Як-от, запит не дозволяє написати $0.completedAt < currentDate, якщо стовпчик completedAt може бути нульовим. (Цей вираз, звісно, не виконується буквально, а перекладається в SQL тими самими макросами.) Та я поки знайшов лише такий дикий синтаксис, щоб це обійти: $0.completedAt.map { $0 < currentDate } ?? false.

Так чи інакше, застосунок принаймні скомпілювався та майже повністю працює.


14.11.2025

Трошки з адміністрування OpenSearch

…Передісторія: так склалося що в одного з індексів OpenSearch цілих 100 шардів на приблизно 5 Гб даних. Стандартні рекомендації — це шарди розміром у 10-50 Гб, тобто десь у 1000 разів більші. Ну буває.

Десь місяців із 9 тому я писав, що це ні на що не впливає, але виявляється, все залежить від навантаження. Отже, стала задача звести кількість шардів до розумної… ну, наприклад, 2.

Робити це “наживу” OpenSearch не вміє. Тобто нам доведеться замінити індекс. (До речі, саме тому в OpenSearch також є псевдоніми індексів (alias) - щоб легко підміняти індекси або цілі набори.)

Є операція reindex. Вона бере кожний документ та копіює його в новий індекс. Триває все це кілька годин. Я вже писав, що все в OpenSearch, що працює на рівні документів — дуже повільне. Під час цього, якщо старий індекс відкритий до запису, накопичуються ще зміни, які наздоганяти мені не захотілося.

Тому є інша операція: shrink. Оце вже краще: вона на рівні структур бази обʼєднує шарди. Працює замість годин — лічені хвилини. Але ж не все так просто.

Перепоною стало те, що для виконання shrink всі шарди індексу повинні бути на одному вузлі. Для того є інструкція прямо в документації вище. Але на практиці в нас OpenSearch починав цей переніс та не закінчував. Виявилося, що є обмеження на кількість шардів на вузол.

Причому (!) в AWS OpenSearch обмеження це — як ми визначили — залежить не від розміру вузла. Та точно не від налаштувань — такі речі AWS не дає змінити, щоб ми не “наробили дурниць”. Дякуємо!

Ні — насправді AWS OpenSearch прагне зберегти баланс в кластері, а саме рівний розподіл шардів між вузлами. Ну й добре… створили ще один, порожній, індекс на 100 шардів. Після чого перенесення справжніх шардів на один вузол завершилося успіхом. Ну а далі й shrink відбувся без подальших сюрпризів. Роботу зроблено.

Це я ще не торкнувся того, наскільки безпорадним є UI OpenSearch Dashboards, в якому одне світле місце - Dev Console, тобто можливість надсилати до кластера команди HTTP API.


13.11.2025

Sintra тепер англійською

Крайніми вихідними тихенько випустили англійський переклад нашого застосунку для ведення бюджету Sintra.

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

Може здатися, що додати третю мову там, де є дві — легко. Так, це дійсно незрівнянно простіше, ніж додавати другу мову. Але окрім перекладу, довелося також виконати локалізацію. Наприклад: впровадити підтримку тижня, що починається з неділі. Чи форматування сум, де знак валюти йде спочатку: $100.

Величезною підпорою був ШІ, він і зробив чернетку перекладу, і допоміг Саші з локалізацією. Я практично тільки переглядав зміни… та займався окремо застосунком на React Native. Бо в застосунків для iOS своя система локалізації. А ще є App Store Connect, а там кільканадцять сторінок, де теж можуть ховатися не перекладені рядки. От виявив, що ми забули про переклад посилання на політику приватності — і таке є!

До того ж в App Store є така річ, як “мова за замовчуванням”. Нею застосунок світиться в різних місцях — я вам не поясню, за якими правилами. Так от, щоб змінити цю мову, треба було спочатку застосунок опублікувати з англійською мовою, потім в App Store Connect змінити налаштування — яке, несподівано, увімкнулося негайно, без додаткового випуску. Тільки довелося чекати, поки всілякі кеші прочистяться.

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


12.11.2025

Рухоме середнє

Є в мене утиліта split_tests, вона вміє розбивати пакет тестів на частини рівної тривалості. Для того тривалість кожного файлу передається в широко відомому форматі JUnit XML.

Та все б гарно, але тести не завжди мають ідеально рівну тривалість. (Особливо інтеграційні.) Та оскільки ми спираємося на результати останнього запуску, де-факто розбіжність між частинами складає до 20%. Знаєте, як дратує покладатися на емпіричні виміри, та все одно отримувати таку розбіжність?

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

T нова = 0.9 * T стара + 0.1 * T поточна

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

Отже, тепер у split_tests є новий режим:

split_tests -junit-update=old/*.xml -junit-new=new/*.xml -junit-out=updated.xml

Достатньо цю команду додати в кінець збірки, перед збереженням даних, та тривалість стане стабільніше! Наскільки? Це мені ще доведеться визначити.