Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
24.12.2023
Дев-адвент 24: звʼязаний список
🎄 Всіх з Різдвом, хто святкує! Дізнався, що в App Store вже кілька років немає різдвяних канікул, та публікувати програми можна протягом всіх зимових свят. Що приємно, бо до Нового року я як раз дотягну решту функціонала.
Сьогодні нарешті закінчив роботу над збереженням стану системи та інкрементальним оновленням.
Як один з останніх нюансів, знайшов ефективнішу схему зберігання розвʼязку свого алгоритму. Як я писав, алгоритм будує розвʼязки для більших послідовностей через розвʼязки для коротших; але раніше я зберігав кожний розвʼязок наново. Виходила купа даних з квадратичним характером зростання.
Так от, зрозумів, що можна буквально зберігати кожну нову послідовність через номер коротшої послідовності, з якої вона побудована; виходить такий звʼязаний список. Тепер розмір даних зростає лінійно — та ще й без зайвого копіювання. Ба більше, я майже впевнений, що коли ми вчили динамічне програмування, то так воно й зберігалося.
Наступний етап — нарешті, доповнення історії різними цікавими статистичними фактами.
23.12.2023
Дев-адвент 23: правильне керування складністю
Трохи застопорився з оптимізацією обчислень. Хотілося розділяти графік на відрізки, коли є великі розриви в даних (більш як два тижні.) Намагався доробити алгоритм так, щоб він ділив послідовність на декілька. Це є набагато складніше, ніж мені спочатку здавалось.
Висновок 1: я запізно зрозумів, що відрізки між розривами, фактично, ніяк між собою не повʼязані, тому задачу можна розділити на 2 етапи: знайти безперервні відрізки та зробити аналіз по кожному окремо. Натомість я працював над розширенням алгоритму, щоб він враховував розриви.
Ніби в теорії те саме, але в залежності від перспективи можна отримати або ускладнення логіки, або акуратний підхід “розділяй та володарюй.”
Висновок 2: взагалі в мене вже є алгоритм, який працює з розривами — я про нього писав ще 2 тижні тому. Але той алгоритм “не чистий”, бо не робить з розривів “спеціальний випадок”. На практиці це майже нічого не значить - “спеціальність” можна показати вже в інтерфейсі.
Там був ще аргумент за оптимізацію пошуку відрізків, бо якщо ти натрапляєш на розрив, то раніше можна не шукати. Але, цей пошук і так і так має лінійну складність, тож багато не оптимізуєш.
Підсумок: треба було розв’язувати проблеми одна за одною, а не замахуватись разом все оптимізувати.
22.12.2023
Дев-адвент 22: async/await у Swift
Як і в JavaScript, у Swift є синтаксис async/await
. Він зʼявився два роки тому - Swift молода мова, та не стримується від капітальних змін.
Async/await - одна з моїх улюблених особливостей JavaScript, та від Swift очікував чогось схожого. В базовому розумінні це дійсно одна й та сама фіча — вона надає можливість писати асинхронний код в послідовному стилі. Та як і в JavaScript, так і у Swift це робить код легшим до розуміння.
Втім, є важлива різниця. У Swift немає загально прийнятної абстракції Promise. Попереднім способом побудови асинхронного коду були колбеки. Проблема в тому, що немає способу замінити колбек на async/await
- для цього потрібно переписати функцію, яку будемо викликати. Тому бібліотеки з підтримкою async/await
зʼявляються тільки поступово.
Та ще й потрібно дізнатися, що альтернатива існує та знайти її. Наприклад, в Apple Health для переходу на async/await
потрібно замінити HKSampleQuery на HKSampleQueryDescriptor.
Так що тепер ще більше ціную те, як JavaScript стандартизувався навколо Promise, бо вони не просто замінили колбеки, а ще й стали універсальним протоколом, який можна комбінувати як заманеться.
21.12.2023
Дев-адвент 21: все програмування — це побудова абстракцій
Успішно розвʼязав вчорашні проблеми та ще декілька — завдяки тому, що впровадив правильну абстракцію для дат.
Так, я хотів спочатку зробити тип “календарна дата”, як я робив для JavaScript. Але вранці придумав простіше та краще рішення: перекласти дати в порядкові числа.
Взяв за 0 перше січня 1970 - початок “епохи Unix”, і далі послідовно. Поточний індекс — лише 19712… тобто можна було б взяти початкову дату набагато раніше. Вийшов ось такий маленький клас.
Така абстракція чудово підходить для моїх обчислень, бо вони відбуваються за послідовністю дат. Відстань між датами обчислити легко. Зберігати тривіально.
Це порівняно з використанням стандартного класу Date
, який насправді зберігає момент в часі, та з яким потрібно було завжди памʼятати, чи звів я момент до початку дня, а також ризикувати натрапити на проблему літнього часу, або високосну секунду…
Проблема не в тому, що це неможливо, а в тому, що доводиться тримати в голові інформацію, зайву для поточної задачі. В програмуванні так відбувається часто, та розвʼязок один — рефакторити так, щоб бачити тільки ті подробиці, які потрібні. Вирівнювати абстракцію.
Та в тому, думаю, й полягає успішність сучасного програміста — не хитрі алгоритми або енциклопедичні знання бібліотек, а вдале керування абстракцією.
20.12.2023
Дев-адвент 20: інкрементальне оновлення — це складно
З вчора, як писав, залишилась задача зробити інкрементальне оновлення. Тобто нові виміри я вже зрозумів, як отримувати, але ж треба їх застосувати до вже обчислених з мінімальними витратами.
Оновлення приходять з HKAnchoredObjectQuery
у вигляді двох списків: доданих записів та видалених записів. Ну, хоч редагування в Apple Health немає. Зате користувач може додати або видалити записи з будь-якого місця історії — наприклад, імпортувати старі дані.
В загальному вигляді інкрементальне оновлення зробити складно. Тому беру урок у Redshift та використовую інкрементальне оновлення тільки в прямолінійному випадку. Для інших випадків роблю повне обчисленння, яке вже готово і працює.
Для мого алгоритму “прямолінійний” випадок — це поява даних на наступні дати. В тому сенсі, що така зміна і є інкрементальною. Але також і в тому, що саме таким буде типове щоденне оновлення.
Мій внутрішній перфекціоніст каже, що належно обробити кожну можливу ситуацію та зробити алгоритм інкрементальним для всіх випадків. Втім мій внутрішній менеджер заперечує: на це немає ані часу, ані потреби.
До того ж оновлення нелегко зробити й в простому випадку, бо треба боротися з датами, а типу календарної дати у Swift немає, і може варто було б вже зробити його власноруч.
19.12.2023
Дев-адвент 19: збереження стану
Як вже казав, завантаження даних з Apple Health та їх обробка займають невеличкий, але помітний проміжок часу. Це значить, що старт програми гальмує. Розвʼязок простий — зберігати вже оброблені дані, щоб не обчислювати все наново.
Саме збереження-відновлення зробити нескладно. Обрав як метод серіалізацію в JSON. У Swift прості структури можна серіалізувати, якщо навісити на них тип Codable - решта робиться автоматично. Я вже писав, що запис у файл — це метод на рядку, що дуже дивно. Насправді ще є клас FileHandle з більш звичним API читання-запису.
Нехитре збереження стану в файл JSON роблять завантаження під час запуску непомітним. Ніби справа зроблена — але не до кінця. Потрібно ще доповнити дані тими, що могли зʼявитися після останнього запуску програми.
На цей час знайшов брутальне рішення — роблю обчислення в повному обсязі після того, як завантажу початковий стан. Такий підхід вигідний своєю простотою, але даремно витрачає батарею, та й займає час — тобто оновлення даних все ж доведеться чекати, хіба що не дивлячись в порожній екран.
Проте хочеться натомість отримувати тільки нові дані; при чому алгоритм готовий їх прийняти без повного переобчислення. Але як взяти нові дані? Виявилося, що в Apple Health є спеціальний тип запиту саме для отримання нових (для нас) даних - HKAnchoredObjectQuery. Цей запит повертає значення-“якір”, яке можна навіть зберігати між запусками програми. Також його можна виконувати в режимі спостереження, тобто замінити HKObserverQuery
, про який я писав.
18.12.2023
Дев-адвент 18: заповнення порожнеч на графіку
Мій графік ваги (та весь підхід) розрахований на щоденні зважування. Але, звісно, трапляється таке, що зважуватися не виходить — наприклад, в подорожах. Виникає питання, що робити з графіком, якщо на цей день немає реальних даних.
Хоч “реальне” значення ваги обчислюється за методом рухомого середнього, але складники цього середнього завжди беруться з попередніх дат — тож якщо даних на дату немає, отримаємо не інтерполяцію, а просто майже сталу лінію — обчислене значення наближатиметься до останнього відомого, от і все.
Коли йдеться про найсвіжіші дні, то можна взагалі нічого не малювати, та чекати, поки ми повернемось з відпустки та внесемо наступну вагу. А от з порожнечею між відомими значеннями вже можна щось зробити.
Є два випадки. Якщо порожнеча довша за вікно рухомого середнього, то наступне після неї значення буде, фактично, початком нового графіку. Таку порожнечу краще нічим не заповнювати, а навпаки, показати як розрив. Це трохи технічно складно, тому поки я цим не займався.
Інший випадок — коли значення після порожнечі все ще коректується минулими значеннями (як на ілюстрації.) Як можна побачити, наївний алгоритм просто робить стрибки в графіку там, де зʼявляється нова інформація. Більш природно інтерполювати обчислене, “реальне” значення, що я й зробив.
Все це суттєво складніше, ніж просто зʼєднувати точки лінією, як це робить Apple Health. Зате отримуємо: 1) наочну динаміку та 2) відсутність штучних стрибків, які цю наочність підривають та примушують нас вмикати мозок.
17.12.2023
Дев-адвент 17: фокусування
Зрозумів, що якщо я хочу зробити привабливий MVP та ще й в найближчі 8 днів, доведеться обмежити функціонал до дійсно мінімального. Це значить, залишити тільки графік та аналітику по ньому.
*(Треба памʼятати, що продукт з однієї чудової функції більш привабливий ніж з однієї чудової та пʼяти недороблених. Дивись ефект менше-краще.)_
Тож зараз працюю над генерацією текстів пояснення за результатами аналізу (такими, як “скидаєш вагу з 10 грудня, такими темпами досягнеш мети через 2 місяці.”.)
До речі, все ж відмовився від регресії як оцінки проміжків. Окрім того, що це повільно, як писав вчора, то ще й результати не краще ніж мій попередній метод. В деяких випадках - навіть гірше, бо найкраща регресія в мене не обовʼязково закінчувалася на піках графіку.
А регресія знадобиться для визначення поточного тренду. Тобто для того, для чого її придумали.
16.12.2023
Дев-адвент 16: інтеграція регресії
Отже, вчора обрав влучні параметри функції ціни, щоб покривати графік відрізками регресії. Залишилось повернутися назад в головну програму та зробити робочу версію.
В чому проблема: я писав, що придумав, як уникнути кубічної складності при обчисленні розвʼязка. Поясню. Сам алгоритм динамічного програмування має квадратичну складність: один цикл для збільшення діапазону розвʼязку, а всередині — ще один, для перебору з розвʼязків для менших діапазонів.
Регресію я знайшов, як робити паралельно з внутрішнім циклом. Але оцінка якості регресії — теж цикл; необхідно порівняти значення в кожній точці. Виходить, що щоб оцінити, який розвʼязок найкращий, доведеться зробити ще один цикл — оцінки регресії.
Квадратична складність на декількох тисячах точок — не проблема для сучасного процесору, а кубічна — вже займає помітний час, декілька секунд.
Все це значить банально те, що розвʼязок потрібно зберігати, а не обчислювати наново щоразу. Бо минула вага є константою, тож і розбиття на відрізки теж. Хіба що у випадку редагування або імпорту старих даних доведеться обчислити повторно.
А решта мені подобається — модель нарешті математично акуратна.
15.12.2023
Дев-адвент 15: цінність прототипування
📈 Нарешті, взяв експортовані дані та зробив з ними тестовий каркас. Щоправда, обрав не Excel, а JavaScript та D3.js, бо тут мені звичніше.Отримав за годину більше прогресу ніж за дні абстрактних міркувань та розглядання чисел.
Щоб зробити дещо просте в D3, достатньо одного файлу HTML. Скрипт — там же ж. Дані — там же ж. Ніякої збірки. Prettier легко впорається з автоформатуванням. (До речі, щоб не чіпати CSV, перетворив їх на JSON через один рядок Ruby.)
Відтворив алгоритм на JavaScript, та зробив так, що кожний крок робиться або за таймером, або натисканням кнопки. Мені було потрібно зрозуміти поведінку регресії та підібрати функцію ціни. Побудова регресії на графіку робить поведінку буквально очевидною.
З функцією ціни трохи складніше, бо треба уявляти поведінку математичних функцій. Наприклад, мені вже відомо, що щоб алгоритм віддавав перевагу довшим відрізкам (замість декількох коротких), можна взяти ступінь від довжини. Але яку ступінь? Це легко зрозуміти, якщо результати відтворювати на графіку.
На мою думку, досвідчений інженер повинен вміти робити не тільки великі, красиві та акуратні рішення, а й працездатні рішення в найкоротший час. Це не тільки експериментів стосується, а й скриптів для разових дій, прототипів, досліджень.