Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
30.05.2025
Я втомився від Capybara
В Ruby on Rails надто зручна система для інтеграційних тестів — з Capybara. Вона дозволяє керувати браузером з Ruby. Та це чудово. Можна поєднувати в одному файлі будь-які налаштування застосунку та бізнес-логіки та всі можливості браузера.
Бо знаєте, не всякі побічні ефекти можна прочитати не те що з інтерфейсу, а навіть з API. А у нас, оскільки інтеграційні тести повністю на Ruby, то можна все що завгодно перевіряти. Стан бази та інших систем — звісно. Але також легко замокати все що завгодно та перевіряти, що відбулися очікувані виклики.
А ще в Capybara приємний DSL, абстрагований майже від всіх нюансів вебсторінок. (Хоча за потребою можна й спуститися на низький рівень чи навіть виконувати JavaScript.)
Такі зручності формують стиль тестів, де браузер для нас — лише ще один спосіб перевірки. Тобто головний фокус тестів залишається на застосунку, єдина різниця що для цих тестів ми ним керуємо з браузера — на відміну від юніт-тестів, де код викликається напряму, та тестів запитів, де ми робимо прямий запит до API застосунку.
Але браузер — річ складна та хаотична. Абстракція тече, та замість простих “перевірок браузером” ми починаємо латати в ній дірки. Хоч є багато системних покращень (як-от Capybara вже давно вміє автоматично чекати, доки на сторінці зʼявиться зміст, який ми перевіряємо), але код тестів вкривається латками все одно.
Минає час, та ми отримуємо незграбну, незрозумілу масу коду, який ще й все одно регулярно дає хибу.
Який я можу зробити висновок… Capybara була чудовим інструментом на той час, коли JavaScript був рідкістю (так, колись взагалі окремо виділяли тести, яким він потрібний — решта навіть браузер не запускала!) Але зараз фокусом інтеграційних тестів є застосунок JavaScript, та набагато легше було б їх писати не на Ruby, а наближено до браузера — тобто в Cypress чи Playwright тощо. Тим паче, що для них вже давно зробили інтеграції, які вміють запускати фабрики, а то й довільний код на Ruby.
29.05.2025
Агент ШІ: Від 0 до вайб-кодінгу
Часто доводиться чути, що використання ШІ в програмуванні — це обовʼязково радикальний відхід від норм. Це все одно, як думати, що якщо купиш болгарку, то нею треба буде різати все, від хліба до нігтів. Ось вам декілька сходинок поступово більшого використання ШІ — обирай ту, яка тобі комфортна.
1. Ставити питання щодо проєкту. Як працює X? Чому тут так написано? Яка різниця між А та B? ШІ здатний швидко обробити багато коду та дати чудові пояснення. При цьому ШІ не пише аніскілечки вашого коду, тому й ніяких ризиків немає. (До речі, тут та далі потрібний редактор з ШІ, як-от Cursor, а не просто чатбот без знання проєкту.)
2. Маніпуляція тексту. Моя улюблена функція поки — це скопіювати шлях до об’єкта в Google Console та попросити зробити з нього виклик CLI. Задачі такого рівня вимагають нудних ручних перетворень, а ШІ з ними порається дивовижно.
3. Локальні доповнення. Це коли тобі пропонує продовження рядка, який ти зараз пишеш. Я цим користуюся багато років з TabNine, але чесно кажучи, LLM краще радять, більш поглиблено.
4. Локальна генерація чи рефакторинг. Можна попросити згенерувати одну функцію, чи скелет класу, чи тесту. В Cursor для цього тиснеш Cmd+K
в потрібному місці. А ще - виділяєш текст, тиснеш Cmd+K
та рефакториш на місці. При цьому зміни не залишають область виділення, а якщо не сподобалося — можна відразу скасувати.
5. Покрокова генерація за інструкціями. Це коли ти агенту кажеш “згенеруй мені клас”, “згенеруй тести для нього”, “пересунь функцію в інший клас” - та коректуєш після кожного кроку. Виходить програмування в напівавтоматичному режимі — машина робить нудні речі, ми дописуємо важливі.
6. Покроковий рефакторинг за інструкціями. Я виділив в окрему сходинку, бо рефакторинг має більше ризиків попсувати код, тому потрібно ретельніше переглядати його результати. Зате в цілому можна робити навіть на кшталт “прибери повторення”, або “знайди схожу функцію в інших місцях проєкту та використай її”. Так що не слопом єдиним!
7. Генерувати плани за допомогою ШІ. Цим я сам ще не займався. Але технічно ШІ так само може генерувати план, як і код (або навіть краще.)
8. Піддатися та прийняти вайб. Ну слухайте, я досі не вірю, що хтось це робить навсправжки.
28.05.2025
Сліпці та AI слон
🤼 Надзвичайно важко говорити про генеративний AI щось раціональне для загальної аудиторії. Бо визначні голоси поділилися на релігійних AI-оптимістів та таких саме релігійних AI-заперечувачів. Та, як завжди, реальність десь посередині. Щоб побачити її потрібно позбавитися багатьох хиб, які натомість вбиваються дедалі глибше з протилежних сторін. Ось мої думки, як релігійного прагматика.
Спочатку про оптимізм. Слон в цій кімнаті — це те, що генеративний AI став поточним бумом, а значить, на ньому заробляють купу грошей. Не користувачі, а насамперед постачальники (як-от компанія OpenAI). Не з користувачів, а з інвесторів. Поки є оптимізм — будуть інвестиції. Гроші ллються на підґрунтя двох міфів.
Перший міф — що генеративний AI здатний найближчим часом переступити за межі перетворення запиту на результат та стати чимсь більшим. Як я це розумію, для того потрібний черговий науковий стрибок. Масштабуванням з ліхтаря ніяк не зробиш сонце. Це — найсумніший міф, бо пересічні люди проєктують наукову фантастику на реальність, а вся медійно-корпоративна структура їх тільки заохочує.
Другий міф — що на розвиток AI потрібно дедалі більше ресурсів. Це справжня золота жила, бо якщо раніше треба було наймати людей чи принаймні пояснювати інвестору, куди підуть гроші, то зараз все ясно — датацентри! датацентри! датацентри! Та сотні мільярдів інвестицій на них. Тому поява цього року моделі від DeepSeek так розхитала ринок: якщо є дешевший шлях, то, може, не варто спалювати стільки грошей? За логікою споживача, це успіх, але за логікою постачальника — суцільна катастрофа, тому треба скоріше про це забути, бо ось-ось прорвемось, дайте ще грошей!
Це гарний перехід до AI-заперечувачів, бо цю тезу вони охоче підхоплюють. Втім, якщо перевіряти, то виходить, що запити до AI витрачають менше енергії, ніж інші повсякденні витрати. А тому принаймні можна розглядати генеративний AI як реальний інструмент, та дивитися, які переваги він приносить, та які ресурси заощаджує. Знаєте, що ще витрачало багато енергії? Перші ЕОМ.
Та наступний міф заперечувачів — що генеративний AI здатний тільки на нісенітницю. Я розумію, це реакція на розгнузданий AI-оптимізм. Втім достатньо хоч трохи випробувати LLM, щоб побачити, що це не так. Що результат залежить від твоїх запитів та твого нагляду. Звісно, якщо потребувати від LLM всього та відразу, то вийде сміття. Але це все одно що ображатися на перфоратор, за те що він не зробить тобі ремонт.
(Та так, доводиться дивитися на генеративний AI в контексті попередніх бумів — крипти, Web3, NFT, метаверсу — де ніякої практичної користі не було, була тільки бульбашка. І дуже важко було змусити себе подивитися поза цією черговою бульбашкою. Але обіцяю, в AI користь є.)
ОК, на чому хочу сьогодні закінчити — так, для програмного інженера від поміркованого використання LLM є багато переваг. Нехай фантазери мацають слона та базікають, а ми поки його навантажимо та поїдемо у справах. 🐘
27.05.2025
Функціональний стиль чи декларативний?
Зробив цікаву помилку. Це код власної перевірки для бібліотеки для тестування RSpec:
# expect(response).to be_foobar
RSpec::Matchers.define :be_foobar do |bar|
match do |actual|
check_foo(actual)
if bar
check_bar(actual, bar)
else
true
end
end
end
Помилка тут в тому, що якщо bar
немає, то перевірка завжди повертає успіх. Причина наче очевидна, якщо вже про неї знати: блок match
повинен повертати true
, якщо перевірка пройшла. Це звичайнісінька функція. Тому в мене перша перевірка фактично нічого не робить, бо її результат ніяк не враховується.
Мабуть, я це писав під впливом SwiftUI, де все не так:
VStack {
Text("Foo")
if bar != "" {
Text("Bar \(bar)")
}
}
Це вірний код: перший рядок буде видимим завжди, а другий — тільки якщо змінна не порожня. Власне, так саме працює й JSX та інші мови розмітки, але SwiftUI відрізняється тим, що це все нормальний код Swift.
Якби це був лісп — наприклад, Clojure - то там не тільки легко зробити макрос, який би виконував такий синтаксис та збирав результати, а й такого “дивного” більше, ніж традиційних функцій. Для простого прикладу наведу протяжні макроси (кложуристи, допоможіть, бо я трохи призабув):
(as-> result _
(check-foo _)
(and bar (check-bar _))
)
Але у Swift таких потужних макросів немає. Виявляється, заради SwiftUI придумали більш спеціалізований механізм - Result Builder. За наявністю анотації @resultBuilder
він збирає послідовність викликів у масив, а потім надсилає у відповідний інтерфейс. Ось так, не побоялися повністю перевизначати поведінку блоку коду без видимих на те ознак.
26.05.2025
Як не вчитися на програміста
В мене спитали поради щодо навчання в компʼютерній академії IT STEP. А саме, чи це гарне місце для того, щоб юнаку стати програмістом? В самого немає досвіду навчання там, є тільки відгуки рекрутерів, ну й інформація про курс з сайту. Отже.
Те, що я бачу — цей курс триває два роки. Це дуже великий термін для того, щоб навчитися програмувати! Зате — програма намагається охопити таку кількість дисциплін, що в неї немає жодного шансу освітити бодай одну повноцінно.
Як на мій погляд, то це такий гранд-тур програмування. Якщо є гроші та час — то можна “проїздом” побачити все - від скриптів на Python до застосунків на iOS та Android. З такого боку, не так воно й погано.
Але ж далі вони обіцяють, що випускник курсу буде все це вміти робити, та бути готовим шукати роботу… А ось це ніяк не може бути правдою. Мало того, що за такий стислий час можна побачити тільки основи чогось, але й ці знання будуть вилітати так же ж швидко, як на заміну ним приходять нові.
Бо єдиний спосіб засвоїти щось в програмуванні — це практика. Багато практики. В кращому випадку, студент знайде серед тем те, що його захопить, та залишиться там, вивчати глибше. Поза школою. Самостійно. А “перелік засвоєних тем” нічого не вартий та розрахований приголомшити незнайомих з програмуванням батьків.
На заміну я рекомендував знайти курс конкретної дисципліни та почати з неї. Бо сама структура курсів це корисно — викладач, група, плани, терміни — з всім цим легше вчитися, ніж самостійно. Зате з однією дисципліною є шанс дійсно щось вивчити. Хоча все одно, без самостійної практики та інтересу нічого не вийде.
А так… ну не знаю, виправте мене, якщо я не правий, але програмування — справа для тих, хто готовий самостійно колупати незнайомі теми, бо вони ніколи не закінчаться.
25.05.2025
Оновлення дати у SwiftUI
В моєму стохастичному таймтрекері Ping чимало логіки залежить від поточної дати. Почати хоча б з часограми на головному екрані. Це ставить цікаву задачу: як підтримувати дату актуальною? Календарну дату.
Як я дізнався, на платформах Apple є відразу декілька способів стежити за датою — залежно від потреб. Але я був здивований, що один з них — це NSCalendarDayChangedNotification - зроблений саме для мене. Це сповіщення покриває всі випадки зміни дати, яких насправді більше, ніж очікуєш — наприклад, зміна часового поясу. А головне, що не треба вигадувати власної логіки, як колись довелося для React.
Отже, отримати сповіщення від системи — дуже легко. Поширити ці дані в застосунку в сучасному SwiftUI теж легко — завдяки фреймворку Observation. Це проста, але дієва магія. Я створив одинак — джерело дати (тобто просто клас із властивостями currentDate
та ще currentStartOfWeek
), помітив його @Observable
, та тепер кожного разу, як я використовую його у SwiftUI, відбувається підписка на зміни. Байдуже, чи роблю я виклик прямо з компоненти, або десь в глибині коду. Observation лише зберігає відносини між компонентою та всіма атрибутами @Observable
, які були використані.
Оце й усе. Пройшовся по коду, замінив явне отримання дати на звернення до нового DateSource
, та застосунок став помітно уважнішим до часу.
В продовження теми дат, також випускаю мікрооновлення для Reminders2JSON - тепер файл експорту містить в назві поточну дату — бо так зручніше для резервного копіювання. Та іконка теж змінилася. ✅
24.05.2025
Google Workload Identity Federation... на ECS
Понад рік тому я писав про авторизацію з AWS до GCP. ЇЇ не так важко налаштувати та працює вона відмінно. Без жодного секрету, тільки з правильним файлом credentials.json
. Але нещодавно дізнався, що є нюанс: не у всіх середовищах.
Всередині клієнту Google потрібні ключі доступу до AWS. (Щоб побудувати підпис — далі вони не йдуть.) В простому випадку код авторизації (ось на Go) шукає ключі в змінних оточення.
Якщо мовити про виконання в AWS, то ключі в оточенні знайдеш тільки в AWS Lambda. Тому що ключі ці завжди тимчасові. Та тільки Lambda теж настільки ефемерна, що її час існування не перебільшує терміну дії ключів.
А в інших місцях — на EC2 та на Fargate - є спеціальні HTTP API для отримання тимчасових ключів. Різні. Та клієнт Google вміє читати ключі тільки з EC2 Metadata API. Буквально — в їхній реалізації немає згадки про Fargate.
(До речі: намагався знайти офіційне посилання на документацію по цьому Fargate API, та знайшов тільки згадку в документації по SDK. Тобто технічно цей API… незадокументований? Таємний? Це багато пояснює.)
(Взагалі, а чому вони не використовують для цього AWS SDK, в якому вже реалізовані універсальні методи отримання ключів? Включаючи, звісно, читання з цього самого API? Політика?)
Отже, як вийти з цих обставин? Ну я знайшов дикий, але дієвий та універсальний спосіб: підробити EC2 Metadata API, а точніше, його частину, до якої звертається клієнт Google. На щастя, в credentials.json
є посилання на той API, отже, його можна підмінити на наш власний. А власному залишається просто читати тимчасові ключі з Fargate API та віддавати за EC2 API - суть авторизації абсолютно не змінюється. Класичний патерн адаптера. (Але це не точно).
23.05.2025
Залежності — це проблема інтернету
Знаєте, всі ці труднощі із величезними наборами залежностей, а також з розвʼязування їхніх версій — це дуже сучасна проблема. Може тут здаватися, що JS чи інші інтерпретовані мови “завдяки дилетантам отримали таку погану систему”, але насправді це останнє слово прогресу.
Без інтернету проблеми немає тому, що тобі нема звідки взяти нові версії. Люди жили із тим, що є у них в системі, та оновлювали разом із нею, раз на декілька років. Додаткові бібліотеки встановлювалися вручну, з диска, ну або з інтернету, тільки як звичайний архів — і так само вручну оновлювалися.
З одного боку, це люто обмежує — нема в тебе заголовків OpenGL, то й сиди без своєї графіки! З іншого, ті бібліотеки, що були, готувалися із найширшою сумісністю. (А головне, їх було мало, то й проблем було ще менше.)
Я памʼятаю як тільки пробував Ruby on Rails десь у 2006. Було потрібно встановити його з усіма залежностями з RubyGems. Проте з моїм діалапом gem install
, який ходить в інтернет та сам щось стягує, постійно відвалювався. Довелося знайти, як завантажити файли вручну, а точніше, менеджером завантажувань, здатним відновити роботу після відмови (ще й таке було.)
Але головне, що на той час RubyGems це було надсучасно. З PHP в мене просто в проєкті сиділи копії тих кількох бібліотек, які не входили в системний набір. Яка версія була, така й була. Чи була вона сумісною? Якщо працює — то так. (До речі, як збирати щось на C, досі бачиш checking for foo... yes
. ) Іронія в тому, що й зараз версії не гарантують сумісності, а тільки обіцяють її.
Так що яка тут мораль… Що мати багато бібліотек — це надбання прогресу! Та надає безмежний потенціал. Для проблем, але також для творчості. А з версіями ми щось придумаємо.
22.05.2025
Чим система залежностей Go гірша за node_modules?
Тека з модулями Node.js - node_modules
- є на цей час біблейським прикладом надмірної купи залежностей. Але я б не сказав, що ця проблема обмежується тільки Node.js чи тільки JS. Поки подивімося на Go.
В Go пакети завжди імпортуються за URL: import "github.com/leonid-shevtsov/omniwope"
. Що сидить за цим URL? Який формат файлу мають пакети Go? Ооо… Пакет Go завжди є репозиторієм системи контролю версій. (Звісно, найчастіше Git, але не обовʼязково.) Таким чином, мова Go не має ніякого “пакетного формату файлів”, а просто завантажує зміст, як це робить git clone
. Та ще й перекладає питання версіювання на репозиторій.
Це все з одного боку спрощує життя. В Go немає проблеми сквотингу (хоча дехто відхопив собі смачне імʼя github.com/lib/pq, наприклад.) Немає централізованого сервісу — можна хоч на власному домені публікувати. Питання авторизації та приватних пакетів теж легко розвʼязується через репозиторій. Через все це я люблю Go modules
.
(Ще цікавий момент: мажорна версія змінює URL пакету. Хоча це тільки домовленість, та я багато разів зустрічав несумісні зміни з тією ж мажорною версією.)
А тепер, величезний недолік. Змістом пакета є зміст репозиторію. Немає ніякої можливості сказати, що деякі файли не потрібні. Коли ти встановлюєш пакет Go, то тягнеш всю-всю документацію, ілюстрації, тестові файли — просто все. Наприклад: є github.com/pierrec/lz4/v4 із 40 Мб тестових архівів. І всі вони будуть сидіти в тебе на диску, в кеші CI тощо.
Те ж саме стосується й транзитивних залежностей. Та що мене особливо дратує — навіть якщо транзитивна залежність потрібна тільки для оголошення типу, все одно всі її файли будуть завантажені в Go modules. Ну а як інакше, коли тип є частиною коду? Цього можна частково уникнути, якщо оголошувати інтерфейси на боці споживача — але все одно вони часто спираються на конкретні типи. Або, пакети, потрібні для тестів транзитивних залежностей, теж завантажуються. Наприклад, пакет testfixtures підтримує БД Clickhouse. Мені вона зовсім не потрібна — та сам пакет не має на неї залежність. Але його тести — мають! Та оскільки тест сидить в тому ж самому репозиторії, то пакет Clickhouse стає транзитивною залежністю вже мого застосунку. І це тільки один приклад з безлічі.
Заради справедливості: цього напливу залежностей можна уникнути, якщо робити go mod vendor
- тоді копіюється тільки код, потрібний для головного пакета. Але насправді це допомагає тільки якщо у вас цей vendor
ще й доданий до Git. Бо інакше будете тримати локально як vendor
, так і окремо всю купу — щоб його побудувати.
21.05.2025
Тести, дебаг та обережність
Промучився кілька днів із випадковими падіннями тестів. Проєкт великий, незнайомий, а я почав додавати в нього інтеграційні тести з базою. Ну й пішло… один тест — проходить, всі тести — десь плутають стан. Підозр було напрочуд багато.
Спочатку я рухався дуже обережно — рядочок тут, перевірочка там. Щоб нічого не поламати. Згодом збагнув, що поки я запускаю тести локально, я нічого ніколи не поламаю, та можна робити будь-які зухвалі зміни, аби швидше знайти причину.
Підключення до бази зберігалося у внутрішньому пулі. Намагався якось підчищати його, щоб виключити можливе забруднення. Але щоб дійсно перевірити, чи це воно, можна вирізати весь пул з коду та повертати завжди нове підключення.
В коді було багато горутін, які не повідомляли про своє завершення. (Це не такий вже й поганий патерн, коли на кінці виклику ти залишаєш частину роботи на рівночасне виконання.) Чи можуть вони псувати стан для інших тестів? Та додай між тестами очікування у 5 секунд, щоб всі горутіни точно завершились.
Незрозуміло було, де саме відбувається забруднення “чистого середовища”. То й розстав логів хоч після кожного рядка.
А наприкінці виявилось, що я забув, що go test ./...
запускає пакети тестів паралельно. І тільки з опцією -p 1
- ні. Так що на цей раз моя проблема мала дуже просте рішення — яке я не бачив через власні переконання. (І ще через купу інших можливих причин, які довелося відкинути.) 😅