Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni
04.12.2024
Дев-адвент 4: віконні функції у SQL
Сьогодні таке наробив, що аж не віриться, але ж воно працює! Є в мене в трекеру такий графік, як на ілюстрації: він корисний, щоб зрозуміти, яка типова тривалість того чи іншого тегу.
Модель даних тут найпростіша: Sample * ←→ * Tag
. В минулому з SwiftData, щоб отримати таку статистику, доводилося йти за списком проб, перебирати їхні теги та рахувати. Через повільність я дивився тільки на проби за останній місяць. Але тепер все це сидить в SQLite в схемі sample ← tagging → tag
. Ну, думаю, тепер все можна все зробити в SQL без всіляких переборів. Вийшов такий запит на 40 рядків.
-
В серці запита сидять віконні функції. Це єдиний спосіб в рядку отримати доступ до інших рядків. Як-от потрібно зробити, щоб дізнатися, що послідовність тегу почалася чи перервалася: для цього є віконна функція
LAG()
, якою можна підгледіти значення для попереднього рядка. -
Але тільки початок, бо якось потрібно відокремити ті послідовності. Тут я знайшов в Інтернеті такий чаклунський підхід, як
COUNT(*) FILTER (...) OVER (...)
, який для кожного рядка рахує всі попередні рядки, де почалася чи закінчилася послідовність. (ЧомуCOUNT(*)
рахує саме попередні рядки, а не всі? Бо в цьому випадку “вікном” є все від початку таблиці до даного рядка. Та ще езотерика.) В результаті отримуємо таблицю, де все ще по рядку на кожну пробу, але кожна послідовність має унікальний номер. -
Далі, як можна здогадатись, групуємо за номером, та отримуємо довжину кожної послідовності. (До речі, спочатку я думав, що
PARTITION BY isTagged
буде достатньо для цього, та ніяких номерів не потрібно. Але виявилося, щоPARTITION
ділить всю таблицю, не зважаючи на порядок рядків.) -
Ну і тепер все зовсім просто — групуємо за довжиною, отримуємо частоту. Це ми вміємо!
-
Успіх! Така реалізація дійсно спритніше за “обʼєктну”, та ще й встигає опрацювати всю базу, а не тільки останній місяць.
03.12.2024
Дев-адвент 3: SQLite як аналог Redux
Я звик думати про бази даних SQL як щось окреме, відносно далеке та чуже. Але застосування SQLite в останні дні спокушає передивитися ці підходи. Бо тут наче й база даних, але вона локальна та головне, власна.
А ще мені пощастило знайти цю бібліотеку GRDBQuery, яка інтегрує SQLite в компоненти SwiftUI та автоматично оновлює в них дані. Ну прямо як Redux в React!
Як вона це робить? А дуже просто: оскільки всі зміни бази тут відбувається через те ж підключення (це є обовʼязковою вимогою), то GRDBQuery спостерігає за змінами в окресленому “регіоні бази” - за замовчуванням це, здається, залучені в запиті таблиці — та перезапускає обʼєкт запиту для отримання нового значення.
Звісно, я б не зберігав буквально “стан застосунку” в SQLIte, бо хоч вона й швидка, але зміна значення в базі потребує більше бюрократії, ніж просто в памʼяті. Але для “змістовних даних” така модель повністю задовольняє, так що можна забути про всілякі View Model, довіритись “реактивному” оновленню та вантажити дані безпосередньо в компоненти.
(До речі: SQLite працює з файлами на диску, тобто база може бути дуже велика. Але робочу зону завантажує в памʼять, тож доки база маленька, або ми не звертаємось до старих даних — швидкість роботи буде “як памʼяттю”, а не “як з диском”.)
А далі — виходить, що SQL гарна мова для вибірки даних зі стану! Особливо, коли даних багато, а нам потрібні тільки їхня частина або підсумки. Причому не тільки можна перекласти логіку на SQL, а ще й впровадити індекси, чого в умовному Redux немає.
Цікаво, чи не використовують SQLite у “реактивному режимі” у всіляких Електронах, або навіть в вебзастосунках.
02.12.2024
Дев-адвент 2: назви — найважче в програмуванні
Досі не придумав гарну назву для застосунку. Бо модель, яку я взяв за базу, вже має назву - TagTime. Вони навіть оголосили, що якщо ти першим зробиш застосунок, наприклад, для iOS - то назву можеш забирати. Втім, я не хочу бути обмеженим їхньою ідеєю та жорстким дотриманням моделі, тому доведеться імʼя придумати власне.
Але також я тільки-но закінчив величезний рефакторинг по перейменуванню сутності в іншому проєкті. Головною частиною роботи було розбиття роботи на частини, які можна непомітно відкотити, тобто без простою.
Бо “взяти та перейменувати” зводиться до міграції даних та заміни по коду — наче процедура прямолінійна. Але чим далі вона закореніла, тим більше доведеться витратити зусиль, які були б абсолютно зайві, якби відразу обрати правильну назву. Якщо це назва в публічному API чи SDK, доведеться робити нову версію.
Проте ніколи не знаєш, коли внутрішня назва стане публічною. Наприклад, в проєкті на Rails можна невірно назвати модель, а з неї за домовленостями створиться контролер та нарешті шляхи в URL - а це вже всі бачать. Як і назви атрибутів.
Тому окремий талант — обирати гарні внутрішні назви, коли тебе ніхто про це не просить. Мені тут поки не дуже щастить.
01.12.2024
Дев-адвент 1: база даних для Swift: GRDB
Не так склалося, як гадалося, Lighter.swift це більше для баз, які складають зміст застосунку. Я про таке використання навіть не здогадувався. Наприклад, можна зробити застосунок-словник, де зміст буде у базі SQLIte, яка вже запакована в пакет застосунку.
Тому обрав GRDB - це просунутіша бібліотека для роботи з SQLite. Чимало нагадує ActiveRecord з Ruby. Має інтеграцію у SwiftUI - GRDBQuery. А порівняно зі SwiftData - це все ж звичайний ORM: будуємо запити, отримуємо структури, зберігаємо назад. Перебіг даних передбачуваний та знайомий.
Схожість з ActiveRecord: Міграції, за синтаксисом та роботою дуже схожі на Rails. Це відразу вища оцінка. Асоціації, практично так само задаються… хоч лінивого завантаження тут немає. Зате є вбудована вибірка та навіть агрегації за асоціаціями — тільки окремим запитом. Побудова структованих запитів на кшталт Arel - хоча як і в AR, можна завжди зробити прямим текстом SQL, що новачку просто рятує життя.
Про запити. Сподобалося, що можна вказати тип для результату будь-якого запиту. Наприклад, вищезгаданий розмір асоціації треба кудись повертати: для того оголошуємо нову структуру:
struct TagAndCount: FetchableRecord {
var tag: Tag
var sampleCount: Int
}
Tag.appending(Tag.samples.count).asRequest(of: TagAndCount.self)
Про модель застосунку Також сподобався оцей GRDBQuery
. Він автоматично виконує запити, потрібні компоненті SwiftUI. Особливо гарно, що запити для нього задаються у вигляді структурного типу з методом fetch(db)
. Тож в нього можна запхати не тільки запит, а й перед- та післяобробку. Наприклад. я в одному місці перетворюю результат на Set
, бо компоненті так зручніше.
Поки націлений всю логіку переписати на шар запитів, тобто ніякого стану не тримати окремо від бази. Бо та база SQLite теж сидить поруч в памʼяті, до чого ще треба звикнути.
30.11.2024
Сховище даних для таймтрекера
Я зараз готуюся до серйозної роботи над застосунком для таймтрекінгу, та напевно почати доведеться з перероблення моделі та сховища даних. Бо SwiftData/CoreData мене все ж бісить. Вона, мабуть, гарна для простого випадку, коли все що потрібно — це показувати екран списку та екран запису. Тоді SwiftData інтегрується безпосередньо у SwiftUI. Але як тільки зʼявляється складніша логіка — як-от агрегація, розрізи, особливо із застосуванням великого обсягу даних — то вона починає тріщати. Тому дивлюся на альтернативи.
З іншого боку в мене є досвід підходу модель в JSON, тобто коли весь стан застосунку завантажується та зберігається однією великою структурою даних. Це зручно, проте також обмежує дані за розміром, бо я подивився та дані про теги за останній рік вже сягнули декількох мегабайтів, та зберігати такий JSON на кожну дію не хочеться.
…Логічною серединою мені бачиться SQLite; зокрема, всіляку статистику можна обчислювати за допомогою SQL, а в памʼять завантажувати тільки те, що буде видно. У Swift є декілька бібліотек для SQLite, я поки схиляюся до Lighter, бо вона надає типізовані записи, але в іншому не перетягує абстракціями.
До речі, в базі CoreData взагалі немає агрегацій. Бо це обʼєктна база; хочеш агрегацій — будь ласка, ходи по графу асоціацій. Наприклад, така тривіальна в SQL задача, як “покажи найбільші 10 тегів за кількістю використань”, тут вимагає зберігання кількості в атрибут-кеш, інакше буде повільно та витратно. І це ще проста задача; а як щодо “покажи карту використань тегу за годинами та днем тижня”? В такому важкому на статистику продукті з SQL піде веселіше.
29.11.2024
Сталкер 2
Що в Сталкері чудово: мальовничі та рідні пейзажі та атмосфера. Відкритий світ, наповнений цікавими місцями. Можна просто вештатись територією, досліджувати памʼятки та час від часу натрапляти на квест, печеру з артефактом, сховок з унікальною зброєю. Але головною нагородою є сам процес та задоволення від відкриття. Сталкер — гра саме про це.
Базова ігрова петля тут відрізняється від традиційної “взяти квест — піти на локацію — виконати — повернутися, отримати винагороду, продати знахідки”, бо ігровий світ великий, а швидкого пересування немає. Більше виходить мандрувати по локаціях та час від часу повертатись на базу.
До того ж в Сталкері майже немає луту на продаж. Та й купувати майже нічого не має сенсу — ти знаходиш більше аптечок, їжі та набоїв ніж можна витратити. Гроші витрачаються на покращення зброї, які не є вирішальними, а також на ремонт, замість якого можна просто знайти щось новіше та крутіше. Чим більше карти дослідиш, тим краще знайдеш зброю та спорядження.
Побічні квести дійсно цікаві (мені запамʼятався один про “гуру”, через поради якого новачки влазять в халепи.) Також сподобались сховки-головоломки: шлях до них змушує поміркувати та приємно дивує.
А головний квест нудний, та не більше ніж просуває нас з локації на локацію. Персонажі та фракції теж клішеві та нецікаві. Старі фракції з першого Сталкера не пояснюються, що принаймні в одному випадку суттєво впливає на розуміння сюжету. Діалогової системи практично немає.
Бої “не чудові, але й не жахливі” (с). З людьми пістолет з глушником поки OP. Як і те, що противники здатні поцілити тебе через найменшу щілину в стіні, аби був line of sight. Але є ще мутанти/монстри… В них (поки?) надвисокий ресурс життя. до того ж більшість з них вступає в ближній бій, а рухливість тут…. не така, як в Doom Eternal. Крутитися та шукати під собою мутанта — нічого веселого. Рятує те, що битися доводиться не так часто. Хоча неуникних боїв з мутантами у квестах вистачає. Думаю, це можна було б виправити тим, щоб мутанти лякалися та відбігали після влучання, а не перли далі, як паротяг.
Про баги та оптимізацію… мені пощастило грати на RTX 4090 через GeForce Now. Та ніяких проблем зі швидкістю я не побачив. Щодо багів, то нічого визначного… окрім того, що в ланці головного квесту зіпсувалася послідовність та зникло “мирне рішення”, коли я вийшов “не тим шляхом” зі стартової зони — світ-то відкритий. Але далі все було добре. Втім, я спокійно ставлюся до персонажів, що палять у противогазі чи “стоять у дивані”.
Взагалі, я просто радий, що Сталкер 2 вийшов та ще й залишився автентичною грою, а не середнячком з лутом, крафтінгом, побудовою баз та деревом прокачування. Хотілося б ще такого ж майбутнього ставлення, як Cyberpunk 2077 отримав від CD Projekt Red. Але наразі святкуймо, бо є чому.
28.11.2024
Не знаю? Або не перевірив?
Безумовно, ознакою мудрості є здатність сказати “цього я не знаю”, а не вигадувати дурниць. Проте час від часу я стикаюсь з тим, що кажу “не знаю”, а насправді воно значить “мені не пригадалося це подивитися чи перевірити.” Та коли ти не на іспиті, має сенс тут зупинитись та дослідити потрібні джерела.
Інколи це значить написати запит до бази — так, може ми й не збираємо якусь метрику, але деколи її можна обчислити вручну. Та ми відразу оперуємо фактами, а не уявленнями.
Або робимо хак навколо чужої бібліотеки, бо вона невідомо як поводиться. Чи все ж залізти в її вихідний код та зʼясувати? Переважна більшість сучасного коду відкрита.
В мене сьогодні трапилось, що вже почав писати пояснення до ПРу з виправленням, але там не хотів писати “не знаю, чому, але воно працює”. Тоді розібрався, що ж таки відбувається в залежностях, та зрозумів, що виправлення моє було хибне, а треба було робити зовсім інше.
Чи є відоме явище, що поки складеш повноцінне питання до StackOverflow, то вже й сам розвʼязок знайдеш.
У фінансистів є належна обачність, а ми чим гірші?
27.11.2024
Keep-Alive та як воно працює
В мене тут намалювалася помилка, здається, через Keep-Alive. А саме, клієнт внутрішнього HTTP API в Go час від часу віддає помилку io.EOF
. Не найкраща помилка, якщо чесно, бо ніяк не пояснює, що відбулося. А відбулося ніби те, що сервер час від часу закриває підключення.
Keep-Alive (“залиш живим”) можна побачити у двох контекстах: TCP-сокетах та HTTP. Давайте спочатку про HTTP. Тут є заголовок Keep-Alive. Такий заголовок з боку клієнта каже “я б хотів використати це підключення більш ніж для одного запиту”. А з боку сервера, “так, я на це згодний”. На цьому участь HTTP в Keep-Alive закінчується. Ба більше, в HTTP/2 та далі цей заголовок взагалі заборонений! Бо в HTTP/2 сталість підключення є нормою. А в HTTP/3 втрачає сенс, оскільки він побудований на UDP, де передача даних відбувається взагалі без підключень.
Але що треба запамʼятати, то Keep-Alive на шарі HTTP тільки інформує, а справжня реалізація Keep-Alive відбувається на шарі TCP. Та для клієнтів API, гадаю, це все ще актуально, бо HTTP/3 з UDP не має сенсу. (А ви як думаєте?)
З TCP теж не все очевидно. Бо в підключення в принципі немає “терміну придатності”, воно буде існувати поки не закриєш. Але зайві підключення споживають ресурси сервера, тому сервер майже певно їх закриватиме. А щоб цього не відбулося, клієнт може час від часу надсилати так звані “посилки keepalive” (а насправді просто порожні посилки), щоб нагадати серверу, що підключення ще живе. (Сервер все одно може закрити таке підключення, але шанси на це менше.)
Для того в налаштуваннях підключення (в Go це в net.Dialer) є параметри Keepalive, які й вказують, чи надсилати такі пакети, та як часто. Я знав, що базовий клієнт Go вже підтримує стале підключення, а тепер я ще дізнався, що він також надсилає Keepalive, але надто рідко для цього серверу API. Отак.
26.11.2024
Несподівані речі з верстки для друку
Досить відомі (для мене) речі: розмір шрифту (обирається такий, щоб довжина рядка була близько 70 символів), відстань між рядками, відстань між параграфами, відступ першого рядка параграфа, поля, колонтитул з номером сторінки, стилі заголовків… все це мало відрізняється від вебу.
Розмір сторінки здивував, бо зазначений був як A5 (148x210 мм), але на практиці виявився 145x200. Але насправді треба зробити 149x204, щоб врахувати технічні поля на обрізку. А коли обкладинку робити, то ще цікавіше, бо там ще буде корінець, а товщина корінця залежить від кількості сторінок.
Ці дрібні зміни розміру, на недосвідчений погляд, нічого важливого не складають. Проте тут виявляється інший нюанс: на відміну від вебу, текст на сторінці є сталою величиною. В нього немає прокрутки та немає локальних обставин клієнта. Це, якщо хочете, вже векторне зображення, а не текст. З одного боку, тому верстка для друку простіша: її достатньо зробити один раз. З іншого боку, перед нами стоїть відповідальність все зробити правильно. Та навіть мінімальні зміни в полях чи інших загальних властивостях документа здатні попсувати всю верстку.
Що я маю на увазі. Обовʼязково стикнешся з ситуацією, де на сторінці “висітиме” один-два рядки. Або сторінка розріже важливий параграф чи список. Або заголовок не влізе в ширину рядка та одне слово перестрибне на наступний. Виправляти місцеві негаразди тексту — це теж верстка. Наприклад, можна на сторінці зменшити відстань між рядками — незначно та непомітно — і тоді на неї влізе останній рядок. Або зменшити розмір шрифту в заголовку. Або навпаки, розрядити текст, щоб він зайняв більше місця та виглядав охайніше.
Це і є для мене найбільшим здивуванням. Я звик думати про верстку (для вебу) як щось однорідне, впорядковане, де є ідеальна шкала шрифтів, вертикальний ритм тощо. А в книжці — звісно, для цього теж є місце — але ніякий вертикальний ритм не вартий висячого рядка. Хоча, я гадаю, можливі чи навіть існують (здивуйте мене ще раз) системи алгоритмічної верстки, які здатні виявити такі місця та внести потрібні корективи.
25.11.2024
Трасування зображень
Розкажу трохи більше про техніку трасування, яку я використовував вчора для карти. Трасування, або обведення — це спосіб перетворити растрове зображення в стилізоване векторне.
Взагалі нічого тут складного немає. Нам буде потрібний графічний редактор з шарами — Фотошоп, Pixelmator, GIMP, Paint.NET. Ставимо оригінал зображення на фон та починаємо обводити зверху всі потрібні деталі. Можна векторними інструментами (лініями, багатокутниками тощо), але можна й растровими — головне, щоб зміни були в окремих від фона шарах. Коли буде готово, вимикаємо фоновий шар та отримуємо чисте обведення. Звісно, якщо щось не подобається, можна завжди знову увімкнути фон та скоректувати.
Я цю техніку перший раз побачив на прикладі портрета, змальованого зі світлини. А ще чимало мультфільмів зроблені схожою технікою — називається ротоскопінг. Одним словом, підхід зі стажем.
Також трасують супутникові знімки, щоб створити векторну мапу. Наприклад. в редакторі OpenStreetMap кожен може спробувати (тільки, будь ласка, відповідально та обережно).
Ще я нещодавно, щоб зробити детальний план квартири (порожньої), сфотографував стіни, виміряв загальні розміри, а потім, переконавшись у дотриманні масштабу, переніс елементи зі світлини на план.
Багато графічних редакторів вміють робити трасування повністю автоматично. Але при тому вони відтворюють оригінал з усіма деталями. Інколи саме це нам й потрібно — наприклад, щоб потім збільшити це зображення. Натомість ручне обведення має сенс робити тоді, коли ми ще й хочемо прибрати зайві деталі або навіть вигадати нові. Наприклад, як на ілюстрації зверху.