Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni
09.12.2024
Дев-адвент 9: кольори та навігація дерева
🌳 Продовжив роботу над деревною мапою. Вже бачу, як цей компонент можна пристосувати, наприклад, для аналізу конкретного тегу. Також, поки зовсім без контролю вкладеності все ж незручно, бо за моїм алгоритмом вкладеність залежить від розміру тегів в даному тижні, а значить, від тижня до тижня вона схильна змінюватись непередбачувано. Думаю, буде в тегу шкала “догори /вище / середнє / нижче / донизу”, яка впливатиме на вибір вкладеності.
🎨 Щоб там не було… Додав таки кольори. Чесно скажу, спочатку я хотів обирати колір з емодзі. Знайшов декілька бібліотек, які вміють це робити (хоч і на JavaScript). Внутрішній програміст так і рвався реалізувати такий нівроку розумний алгоритм. Проте я розглянув приклади в тестері, та зрозумів, що кольори з емодзі не будуть достатньо різноманітними. Тобто це цікавий спосіб додати краплю гармонічного кольору, але ніяк не для розрізнення тегів у звіті.
🎲 Тому повернувся до випадкових кольорів (звісно, з можливістю редагування.) Формула гармонічних випадкових кольорів в мене вже є. Виходить теж не ідеально, зате зусиль встократ менше.
🪜 А навігація за деревом працює так: за тицем в мапу спочатку знаходжу, в який регіон він відбувся (бо мапа — це Canvas
.) Якщо це тег з вкладеністю, то зберігаю його в масив “шлях” та переобчислюю мапу, але вже для вкладених тегів. Без анімацій поки грубувато, але працює.
🖋️ Наступним кроком буде, певно, доповнення мапи текстовою інформацією: фактичними цифрами, для початку.
08.12.2024
Дев-адвент 8: TreeMap
Вчора вже засинав та згадав чудову програму DaisyDisk - вона візуалізує зміст диска через вкладені “пироги”. Та тут же ж згадав іншу чудову програму - SequoiaView - яка років 20 тому будувала мапу диска методом TreeMap. (А якщо вам цікавий сучасний аналог для Windows, то є WinDirStat, я й досі ним користуюся.)
Але досить про аналіз дисків. Мені гадається, що TreeMap краще за інші методи влаштовує потреби мого стохастичного таймтрекеру, за одною причиною: вона ефективно заповнює весь простір екрана. А простору цього на телефоні ну дуже мало. Вкладеним пирогам чи Sankey буде тісно.
Якщо перший раз чуєш про “деревні мапи”, то це розбиття прямокутника на прямокутні частини, які пропорційні заданому розподілу ваг. Зазвичай розбиття рекурсивне. Є декілька алгоритмів для побудови TreeMap. Наївні алгоритми просто ріжуть в одному напрямку та роблять з прямокутника вафлю, що ніяк не наочно. Я обрав Squarify - це жадібний алгоритм, який намагається робити частини квадратніше. Перший результат — зверху.
Про побудову дерева тегів поки придумав робити так: перебираю теги від більшого до меншого; кожний тег “зʼїдає” всі збіжні (та менші) використання тегів; після чого продовжую з рештою тегів. Так деякі менші теги будуть розбиті між батьками: наприклад, може бути “комп/дзвінок” та “телефон/дзвінок”, зате буде досягнуте повне покриття без перетинів. Та, мені здається, так буде зручно досліджувати витрати часу.
Якщо ще додати параметри “завжди показувати тег на верхньому рівні” або навпаки, “ніколи не показувати”, то це покриє крайні випадки — наприклад “робота” значно важливіша, ніж “коворкінг/робота” та “вдома/робота”.
07.12.2024
Дев-адвент 7: щотижневий звіт
📜 Перейшов до звітової частини застосунку. (В мене частина для вводу даних готова на 90%, а для виводу, тобто звіти, десь на 20%.) Зрозуміло, що без гарних звітів всякий таймтрекер буде марними витратами часу.
🥧Гадаю, першим звітом повинен бути “а куди все ж йде мій час?” Оскільки мій метод збирає статистику 24 на 7, то й звіт можна дати стовідсотковий, і це неперевершено. (Є нюанс, що одним з сегментів буде “дані відсутні”, але це теж показник — наскільки ти уважно взаємодієш з трекером.)
⬆️ Початок — зверху. Але відразу спадає в очі головна проблема такого звіту: теги перетинаються, бо кожна проба містить декілька тегів. Та це просто нівечить графік: “макротеги” займають все коло, а деталізовані збиваються в маленьку купку.
🌳 Наприклад: в мене є тег “робота” та теги для проєктів. Але це не є простим деревом: також є теги “комп” та “телефон”, “розробка” та “налагодження” тощо. До того ж я на початку закладав в модель дерево тегів, але знайшов його надто складним в роботі. Бо все ж задачею номер 1 є легкий та швидкий ввід даних. Втім, це значить, що доведеться добудувати цю структуру на виводі.
🤔 Поки в мене тільки абстрактні ідеї, як це робити. Якщо дивитися на цей графік, то, певно, має сенс дивитися, як великі теги розрізати на менші; наприклад, якщо є 30 відміток “робота” та є з них 10 проб “фронтенд”, то можна показати сектор “10: фронтенд” та “20: робота (інше)”; продовжувати до досягнення приємних результатів: наприклад, щоб найбільші 10 секторів були якнайменшими.
06.12.2024
Дев-адвент 6: внутрішня структура SQLite
Сьогодні часу не дуже багато було, тому задовольнився тим, що розібрався у внутрішній структурі SQLite, щоб зрозуміти її в контексті локальної бази даних, де сидітиме більшість даних застосунку. (До речі, в документації SQLite достатньо цікавих технічних деталей, щоб просто читати її для загального розвитку. Рекомендую.)
Щодо представлення записів (рядків таблиці): воно компактне, наскільки це можливо, та чимсь нагадує формати на кшталт msgpack. Для кожного значення відзначений свій тип, як памʼятаєте, але маркер типу складається з одного байту та навіть може містити в собі значення NULL, 0 та 1. Цілі числа зберігаються зі змінною шириною. В іншому, зрозумілий компактний формат, що інколи може бути важливо. Наприклад, JavaScript на ті ж дані витратить в рази більше памʼяті.
Щодо структури таблиць: тут все напрочуд просто та, на відміну від, наприклад, PostgreSQL, однозначно. Таблиця зберігається як Б-дерево. (Б-дерево це дерево, яке мінімізує свою висоту: тобто для БД, кількість вузлів-сторінок, за якими потрібно пройтися від кореня, щоб знайти запис.)
В кожного запису є ідентифікатор (rowid) - ціле число — та воно й використовується для побудови дерева. Цікаво, що якщо зробити ключем поле типу INTEGER PRIMARY KEY
, то воно фактично й буде містити цей rowid
, що пришвидшить пошук за ключем. (В іншому разі, спочатку потрібно знайти rowid
за індексом, а потім вже запис за rowid
.)
Індекси теж зберігаються як Б-дерева, з єдиною різницею, що тут вже значення в дереві можуть мати довільний тип, та це трохи ускладнює структуру даних. Індекси посилаються на rowid
, тож в запитах, що поєднують декілька індексів, можливо фільтрувати записи ще за rowid
, без завантаження.
Нарешті, в SQLite вся база даних міститься в одному файлі, він розбитий на сторінки (зазвичай по декілька КБ), та в памʼять можна завантажувати тільки ті, що потрібні. Що може стати дуже корисним, якщо даних багато, але ми не плануємо всі їх читати постійно.
Отже, що ми фактично отримуємо, порівняно зі збереженням даних в простих структурах в памʼяті? Прозорий спосіб заощадити на памʼяті (відвантаженням на диск), та також можливість швидко шукати з логарифмічною складністю — особливо за наявністю індексів.
05.12.2024
Дев-адвент 5: SQLite - база з динамічною типізацією?
Нарешті мій внутрішній рубіст або джаваскриптівець може зрадіти: я натрапив на базу даних, в якій майже немає типів! Але… чи то гарно? Я ще не визначився.
Власне, в мене в табличці ключем є момент часу, та я хотів переконатися, що в SQLite, як і у звичному для мене PostgreSQL, таймстемпи зберігаються як цілі числа, та відповідно не гірше ніж числа на роль ключа.
⚠️ Сторінка про типи даних в SQLIte трохи спантеличує, якщо очікувати традиційної для БД моделі типів, та не читати першого речення, де сказано, що на відміну від всіх інших баз, в SQLite стовпчики не мають типу, а мають тільки схильність (affinity) до типу. Про це далі. У значень, втім, типи є.
🤔 Типів лише пʼять: ціле число, число з рухомою комою, текст, блоб та нуль. (Мало? Авжеж.) Та хоч в SQLite є підтримка дат, JSON, GIS, але все це зберігається в одному з цих типів. Дати — особливо дивно, бо функції дат приймають як рядок, так і число. Тобто ми легко можемо зберігати дати текстом та навіть не помітити.
🤯 А зі стовпчиками ще дивніше, бо команда CREATE TABLE
дає можливість вказати як тип наче будь-яку назву. Яку ж “схильність” отримує стовпчик? Це (мені навіть дико це писати) визначається за змістом назви… наприклад, якщо в назві є підрядок INT
, то буде схильність INTEGER
. Що для мене важливо, DATETIME
теж можна вказати типом, а схильність при цьому буде NUMERIC
. Що наче гарно, але ні.
😱 Бо навіть зі схильністю NUMERIC
, якщо писати в стовпчик рядок, то він буде конвертований в число тільки коли є числом. А якщо це рядок дати, то він рядком буде і збережений! І наче все буде працювати, тільки 1) з гіршою швидкістю та витратами памʼяті, 2) з загрозою дублікатів, якщо ми також писатимемо дати в чисельній формі, 3) з незрозумілим сортуванням. Я в шоці.
Підсумок: зробив дати чесним полем INTEGER
, увімкнув режим STRICT, де тип значення повинний збігатися з типом стовпчика, та переналаштував кодування дат в клієнті. Порядок.
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 піде веселіше.