Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
17.04.2023
Бот для Телеграму вчиться генерувати посилання на пости в Телеграмі
Ще одно покращення для бота. Якщо до вчорашнього посту посилання на минулі пости вели на сайт, то тепер вони вестимуть на пости в Телеграмі. Але то тільки якщо читати їх в Телеграмі; бо пости на сайті будуть як і раніше вести на сайт. Тобто посилання зберігатиме поточний спосіб перегляду. Все це працює повністю автоматично. Розкажу, як.
Загальна ідея: впровадити особливу розмітку для посилань на пости. Власне, розмітка така вже є — це шорткоди ref та relref в Hugo. Оскільки пости на сайті обробляються саме через Hugo, то іншого варіанту, практично, немає. Посилання виглядає приблизно так: [вчорашній пост](/stendap/2023-04-16/). Залишається тільки додати підтримку такого коду в рендерер постів для Телеграму.
Як я писав раніше, рендерер використовує той самий пакет Goldmark, як і сам Hugo. Тому перша ідея була — використати код Hugo. Я навіть знайшов місце, яке обробляє шорткоди, втім, рендерер Hugo виявився дуже складним, та не модульним. Оскільки мені тільки потрібно переробляти один-єдиний шорткод, то я не став зʼясовувати, як саме витягнути той рендерер з коду Hugo (на що він не розрахований) та адаптувати для мене.
Тому почав копати в бік розширення Goldmark власноруч. Для впровадження власного коду розширяють парсер. Для того треба реалізувати інтерфейс parser.InlineParser, та визначити маркер початку коду, та парсер для нього. Парсер може бути просто регуляркою. (Власне, про це можна та треба велику статтю писати.) Все було б добре, але ні — виявилось, що власний код не може бути всередині адреси посилання. Це досить логічно, якщо подумати, бо в адресі не може бути й жирного шрифту або, наприклад, іншого посилання. Технічно так відбувається тому, що зміст адреси зчитується парсером посилань як єдине ціле, та не передається для подальшого розбору.
Наступна спроба — трансформувати адреси. Для цього є інше місце розширення Goldmark - parser.ASTTransformer. Типовий підхід тут — викликати вбудовану функцію обходу синтаксичного дерева ast.Walk, та виконувати дії над конкретними вузлами. В моєму випадку, якщо бачу посилання, та в адресі сидить шорткод — то його можна регуляркою замінити на справжню адресу. Це майже розвʼязало задачу, от тільки Goldmark має деякі обмеження на зміст адрес, та шорткод в чистому вигляді ламає парсер адреси та залишає посилання необробленими.
Остаточне рішення гібридне. Спочатку регуляркою знаходжу по тексту поста шорткод, та заміняю на форму, що підходить для адреси: PLACEHOLDER/docname/. Потім — пропускаю через Goldmark, та за допомогою трансформера перетворюю цю форму в адреси. Нарешті — після Goldmark знову регуляркою повертаю решту замінених шорткодів назад в оригінальну форму. (Останній крок потрібний, наприклад, щоб зробити приклад шорткоду з другого параграфу.)
16.04.2023
Заголовки постів — тепер в Телеграмі!
Зробив сьогодні маленьку фічу, яку давно хотів. У моїх постів досить давно є заголовки, але вони зʼявляються тільки на сайті та RSS. В Телеграмі заголовків не було, бо мій скрипт їх не підтримував. Нарешті, коли заголовки реалізовані, поділюся проблемами та нюансами.
Заголовки постів, як вони є, містяться в front matter до поста. Вони не є частиною тексту. Так Hugo може показати заголовки на сторінці переліку постів, в RSS, в тезі <title> і так далі. Але для мого скрипту для постінгу в Telegram це проблема, бо він генерує текст посту в Телеграмі шляхом перетворення вхідного тексту в Markdown у HTML; при цьому титул, як і вся інша передня частина, ігнорується. Ну, точніше, не ігнорується, а виноситься в окрему змінну за допомогою плагіну goldmark-meta; але потім титул треба приєднати до поста… або рудиментарним шаблоном, або — як я поки роблю — просто склейкою. Та при цьому не забути про санітаризацію.
Друга проблема — як уникнути масової зміни постів. Річ у тім, що мій скрипт дозволяє не тільки постити, а й редагувати пости. Це практично корисно тільки для виправлень сьогоднішнього посту, але можливість редагувати залишається для будь-якого з всієї історії. Для цього я зберігаю співвідношення між постами в Hugo та в Телеграмі. Щоб не перепощувати всі пости кожний раз, я зберігаю також контрольну суму тексту; якщо контрольна сума не змінюється, то запит до API Телеграму не відбувається. (Взагалі, контрольних сум дві; одна на весь зміст вхідного файлу, а інша — на результативний текст посту.)
Але. Якщо додати в скрипт титули, то текст абсолютно всіх постів зміниться, та всі вони будуть оновлені в Телеграмі. Мені це не подобається — краще, якщо старі пости залишаться як є. Тому додав також версіювання до моєї таблиці співвідношення. Всі пости отримують версію 0, а нові пости, починаючи з цього — версію 1. Скрипт має також поточну версію; пости з версією, що старіше поточної, просто ігноруються. Таким чином, зберігається можливість редагувати, але додавати функції. Тобто відкриває двері для майбутніх покращень.
15.04.2023
Робота з відволіканнями — підсумки тижня
Звітую про підсумки тижневої спроби знизити кількість відволікань.
-
Screen Time - той що від Apple - ненадійний! Довелося його вимкнути повністю посеред тижня, бо він повністю відмовився “продовжувати час”. Я гадаю, що на macOS він взагалі підходить до моделі додатків, бо вікно додатка не є його головною “сутністю”, як воно є на iOS. Треба подумати над альтернативами — можливо, якийсь скрипт, що наглядає за активністю та нагадує про відволікання.
-
Декілька разів вся схема спрацювала, як я й хотів: відволікся — помітив — записав в блокнот поточне питання — сфокусувався та рушив далі. Але таке трапляється рідше, ніж я уявляв. Більш типова ситуація — це коли задача лише трошки складніша за “потік”, та робота перебивається мікровідволіканнями. Тобто відвертанням від роботи на пару хвилин, щоб перевірити повідомлення чи ще щось. Ці мікровідволікання виконують не то функцію короткого відпочинку, не то прийняття рішень. Причина при цьому така сама — складнощі, які вибивають з потоку. Але вони меншого розміру, та вирішуються швидко (наприклад: як назвати функцію; розбивати чи не розбивати код на функції та інше.)
-
Зараз в мене є ідея, що з мікровідволіканнями може допомогти написання плану реалізації (implementation plan.) Це, просто кажучи, інструкція по написанню кода для поставленої задачі. Ідея моя в тому, що при написанні плану можна якнайбільше складних моментів вирішити заздалегідь. Тоді замість задачі, яка коливається від простого написання коду до складних рішень, буде складна частина — планування та проста частина — виконання.
-
Також для ефективної роботи варто мати чергу наступних задач, щоб не шукати кожен раз. Бо кожний пошук задачі це теж шанс відірватися.
На наступному тижні буду працювати з планами реалізації.
14.04.2023
Чим круті канали в Go
Сьогодні переробляв надсилання подій в AWS Kinesis Firehose на пакетний режим, методом PutRecordBatch. В самому по собі API нічого цікавого немає, проте реалізація цього на Go досить цікава.
Оскільки події відбуваються в багатопотоковій системі, то щоб зібрати їх до купи, потрібна якась модель асинхронної поведінки. Та де в гіршій мові треба було б брати мьютекс, в Go є елегантне та простіше в розумінні рішення — канали.
Тож запускаємо окрему горутіну-колектор, де без кінця крутиться цикл та зчитує з каналу нові події. Як тільки подій набереться достатньо — відправляємо їх до Firehose. В такій моделі список подій належить тільки горутині-колектору, тому немає проблем з синхронізацією доступу. Замість того ми плануємо потік даних по каналах — власне, саме канали й будуть єдиним місцем, де події переходять з однієї паралельної горутіни до іншої.
Канали — то моя найулюбленіша особливість Go, хоча, треба зізнатись, місць, де їх доводиться впроваджувати, зустрічається небагато. Головне, що треба усвідомити про канали — що вони синхронні. Цим Go відрізняється від більшості асинхронних моделей, де відправлення повідомлення не гарантує його прийняття (модель акторів, наприклад.) В Go, якщо одна горутіна кладе дані в канал, то можна бути впевненим, що інша забирає їх у той самий час. (Якщо канал не буферизований.) Це дозволяє впевнено міркувати про потік даних в системі. Операції з каналами створюють в програмі точки синхронізації, та багатопотоковий алгоритм набуває схожості до часового механізму. Особливо це корисно, коли треба ізолювати власність над даними — як в сьогоднішньому випадку.
13.04.2023
T-Shaped Full-Stack Developer
Буває що до посади “фул-стек інженер” ставлять зневажливо: якщо одна людина робить все, то вона нічого не зробить добре. Але то, я думаю, найбільш небезпечна помилка — вважати, що фул-стек має все робити сам.
Головна функція фул-стек інженера — мати розуміння всієї технічної складової продукту. Це суттєво спрощує керування розробкою, порівняно з тим, коли кожною частиною займається хтось інший. Фул-стек інженеру можна доручити цілу фічу, без нарізання по підсистемах. Або виправлення помилок невідомо де. Існування ролі фул-стек інженера обґрунтовано потребами бізнесу.
Але ж зрозуміло, що весь стек — це широченна галузь, та ніхто її не знає досконало від початку та до кінця. Тому в хорошій команді (та компанії) фул-стек розробники підкріплюються вузькими спеціалістами — по базі даних, по CSS, і так далі.
Такий підхід не обмежений програмуванням. Наприклад, в медицині є терапевт, що готовий прийняти тебе з будь-якою скаргою. Та від рутинних болячок саме терапевт й лікуватиме. Але якщо виникає потреба в більш серйозних діях, то терапевт делегує лікарю-спеціалісту. Терапевт — то є фул-стек лікар. 👩⚕️
Проте, чесно кажучи, найняти та утримувати вузького спеціаліста — складно. Простіше, коли в кожного фул-стек інженера є своя заглиблена спеціалізація, з якою він може допомогти колегам. Це й називається T-образний інженер. Тобто “—” то є фул-стек, а “|” то є твоя спеціалізація. В більшості випадків T-образний інженер працює над загальними задачами, але якщо в команді виникають складнощі, то перемикається в режим допомоги.
Хороша команда — це там де знаєш, хто в чому спеціаліст та до кого звернутись за допомогою. Продукту немає ніякої вигоди в тому, що кожен самостійно робить поганенький CSS.
(До речі, як на мене, “фул-стек” як назва посади стала архаїчна. Колись, у 2010-ті, всі вебдодатки робились за однією схемою — додаток на сервері, база даних, HTML, CSS, може трошечки JavaScript для блиску. Тож “фул-стек” значило вміння працювати з усім цим. Але зараз стек може бути абсолютно різний, а посада залишилась.)
12.04.2023
Парсинг та обробка HTML в Go
Якщо в Go доведеться парсити HTML, то для того є непоганий офіційний пакет golang.org/x/net/html. Він здійснює потокову обробку документа; тобто ніякого DOM-дерева не будує, а замість того пропонує пройтись по документу циклом та відреагувати на всі потрібні елементи. Це дуже відрізняється від роботи з HTML у JavaScript та від культової бібліотеки Nokogiri для Ruby.
Сучасні вебінженери звикли бачити HTML як дерево, та адресувати це дерево селекторами CSS: div#header a.nav-link. Це просто в розумінні, але складно алгоритмічно: бо потребує спочатку побудови дерева, а потім ще й пошуку по ньому.
Натомість при потоковій обробці ми маємо заздалегідь запланувати, що хочемо отримати з документа, та власноруч стежити за станом обробки. Наприклад, коли зустрінемо тег div, то перевіримо, чи є в нього атрибут id==header, та якщо є, увімкнемо змінну inHeader = true. А коли зустрінемо тег a, то перевіримо, чи увімкнена змінна inHeader, та якщо так, запишемо в масив navLinks.
Якщо ми хочемо отримати з документа не єдиний “селектор”, а декілька, то стану стає багато, та відстежити що до чого стає складніше. Тому потокова обробка тільки для особливо вибагливих до швидкодії випадків.
Але є ситуація, де потокова обробка прямо набагато краще. Це коли документ ми змінюємо. Річ у тім, що розбір документа в дерево — операція зазвичай незворотна; вона втрачає нюанси форматування, та особливо — помилки синтаксису. А з таким неформатним форматом як HTML, помилка синтаксису цілком може бути помилкою тільки для даного парсера — та її “виправлення” навпаки документ поламає (прибере “неправильний” тег, наприклад). Тому й варто нічого не міняти без потреби.
З потоковою обробкою оригінальний зміст документа не втрачається; те, що нам не цікаво, ми просто копіюємо у вихідний документ — байт в байт. А зміни можна робити як на рівні блоків, так і на рівні тегів або навіть атрибутів — як забажаємо.
11.04.2023
Підтести в Go з функцією T.Run
Сьогодні весь день писав тести на Go. Та, на сьомий рік написання тестів, нарешті дізнався про механізм створення підтестів: t.Run.
Нащо воно потрібно. Зазвичай в Go всі пишуть так звані матричні тести. Це коли всі параметри тесту та очікувані результати складаються в структуру, робиться масив таких структур, а потім цикл бігає по масиву та перевіряє код на кожному випадку. Для більш гнучкої мови програмування це може й дивно виглядало б, але на Go дуже зручно, бо копіпаста скорочується до мінімуму.
Я теж постійно використовую матричні тести. З ними є маленька проблемка: коли тест падає, то незрозуміло, в якому саме випадку. Тому я зазвичай до кожного очікування дописував повідомлення, по якому можна знайти випадок, якось так:
assert.Equal(t, e.expectedResult, myCode(e.param), e.name)
Та ще інша проблема: немає можливості запустити окремо один з випадків. Хіба що вдало закоментувати все зайве. От тільки в Go незручно коментувати шматки коду, бо компілятор почне журитися з невикористаних змінних.
Торік дізнався про існування пакета testify/suite. Їм можна загорнути набір тестів у спільний контекст, такий, як підключення до бази даних. Саме suite мене й наштовхнув на роздуми — а як це вони роблять тести у вкладеному форматі - Suite/TestName? Виявилось, що це вбудована можливість тестового середовища Go, називається підтести, та створюється функцією T.Run.
Підтести це те, чого мені не вистачало в матричних тестах, бо тепер достатньо просто загорнути тіло тесту в t.Run та отримати як підпис кожного підтесту в результатах, так і можливість фільтрувати їх ключем --run. А з пакетом testify/suite можна ще й додавати до підтестів контекст.
for _, e := range examples {
t.Run(e.name, func(t *testing.T) {
assert.Equal(t, e.expectedResult, myCode(e.param))
})
}
Може, це й щось очевидне, але я загуглив пару топових туторіалів та вони матричні тести показують, а підтести — ні. Отакої. TIL.
10.04.2023
Оновлення для go-global. Особливості рефлексії для типу map в Go.
Сьогодні зробив для go-global - нашого пакета для завантаження конфігурації з AWS Parameter Store - маленьку фічу. Пакет цей бере дані з Parameter Store. Там вони мають пласку форму “ключ-значення”, причому значення можуть бути тільки рядками. Але ми хочемо отримати їх у вигляді конфігураційної структури з вкладеністю та з типізацією. Для цього й існує go-global.
Але річ у тім, що він не підтримував тип map - тільки структури та, з недавнього часу, масиви. Структури це добре, коли всі ключі відомі. Але зʼявилася потреба завантажувати словник з невідомими ключами.
Здавалося б, закинути дані в map - це ще простіше, ніж у структуру. Навіть не треба шукати поле за тегом. Але насправді це невеличке розширення функціонала призвело до серйозного перероблення та переосмислення всього підходу.
Як воно взагалі все працює: через рефлексію. В першій версії бібліотеки я крок за кроком спускався по конфігураційній структурі, поки не знаходив reflect.Value, що відповідає місцю, куди треба записати параметр. А потім записував в нього значення. В принципі нічого складного там не було; найскладніше — це, мабуть, створення нових екземплярів обʼєктів, коли стикаєшся з нульовими вказівниками.
Але. Тип map, як виявилось, все це ламає. Бо він в Go особливий. Елементи map, не підлягають адресації. На них не можна зробити вказівника — це раз. Але також, як наслідок, при рефлексії неможливо отримати таке посилання на елемент, яке дозволяє запис. Елементи треба записувати спеціальною функцією SetMapIndex. Так що мій рекурсивний пошук місця призначення більш не підходив.
(Обурений екскурс. Ці приховані особливості всіляких типів — те, що я в Go понад усе не люблю. Абстракція “простих” типів тече. В нульовий слайс можна записувати, а в нульовий меп — ні. Оці проблеми з адресацією. І таке інше… Після того, як я нахвалюю Go, соромно початківцям про ці деталі розповідати.)
Спочатку хотів синтезувати якийсь замінник для “адреси” місця призначення. Може, повертати функцію-сеттер. Але так і не зміг розвинути цей підхід.
Натомість перегорнув весь алгоритм догори ногами. По-перше, тепер я спочатку будую з параметрів абстрактне дерево. Потім, замість пошуку кожного параметра окремо, я роблю обхід цього дерева в глибину, та наповнюю конфігурацію, починаючи з найглибших частин.
Що це дає: коли справа доходить до типу map, то ми спочатку повністю наповнюємо значення кожного елемента, а потім записуємо його в map функцією SetMapIndex. Якось так. В результаті вийшло навіть не дуже складно, хоча без повного переосмислення я б до такої версії не прийшов.
Та, до того ж тепер я відокремив всю цю складну логіку від конкретно AWS Parameter Store, та її можна досконало протестувати. А в майбутньому, може, й додати інші сховища; взагалі для цього є Viper, але в нього філософія відрізняється від нашої.
09.04.2023
Почав робити фотозвіт про походи
В рамках фреймворку “хочу — не хочу” взявся, нарешті, за звіти про подорожі. Річ у тім, що в мене лежать сотні красивих та цікавих світлин, з яких давно мрію збирати звіти, але, як це буває, ніяк не знайду часу.
Одним з аспектів прокрастинації є те, що хочеться відразу дуже технічно крутий підхід, на кшталт статей з New York Times з анімаціями, картами, крутою типографікою і так далі. Звісно, все це я не вмію робити. Тому, щоб зрушити справу, хочу почати з чогось простого На жаль, я не ідеальний, тому найпростіше рішення — пост зі світлинами — мене не задовольняє. Як не задовольняє і галерея без тексту.
Зі зробленого сьогодні: обрав похід (а це теж не так просто; треба було щось ізольоване, але цікаве); відфільтрував світлини від дублікатів та сміття; написав текст. Тепер залишилось зібрати все до купи та, напевно, видалити ще багато фотоматеріалу.
Для перегляду світлин обрав поки lightGallery. За задумом галерея має бути не одна, а по одній на кожний параграф, та ще й з цікавим макетом. Тож наступна технічна задача — організувати структуру документа з тексту та галерей. Та, в ідеалі, щоб все залишалось в Markdown.
Знайшов отакі рекомендації по розміру картинок від Shopify; виходить 2240x1260 пікселів як максимум. До речі, щоб підігнати картинки під розмір однією командою — беру ImageMagick - командою mogrify -resize 2240x1260 *.jpg. А для остаточної оптимізації розміру є утиліта ImageOptim, якою я теж постійно користуюсь.
Ще є корисний додаток PeakFinder, який підписує гори; причому він вміє це робити як по камері, так і по старій світлині. Та, він навіть працює на macOS, хоча і в версії для iPad.
(Якщо є готові, відкриті інструменти для створення матеріалів “як у Нью-Йорк Таймз”, буду дуже рад дізнатись.)
08.04.2023
Що робити з рефлекторними відволіканнями?
Отже, вчора я зʼясував, що головна перешкода в роботі — то рефлекторні відволікання, що виникають, коли натрапляєш на складне місце. Поставлю мету їх позбавитись. В ідеалі, робота над задачею має починатись, тривати безперервно, та закінчуватись. Або свідомо перериватись на відпочинок. Але не на спонтанну перевірку новин.
(О, до речі, всі активні відволікання — тобто повідомлення та інше — в мене давно вимкнені назавжди — окрім найкритичніших. Бо фокусуватися на роботі, коли булькають месенджери — це взагалі для мене неможливо.)
Як вже писав, спочатку треба зупинити себе. Поставити, так би мовити, breakpoint. В цьому допоможе Screen Time. Він потрібен не для того, щоб все заблокувати та викинути ключі — я доросла людина. Головна мета тут — побачити, коли відвертаюсь. Блокую ресурси — додатки чи сайти — на які заходжу частіше за все. (Список таких ресурсів треба неодмінно оновлювати за актуальністю.)
Тепер, наступного разу, як під час роботи побачу екран блокування, задам собі питання — чому відвернувся? Що стало причиною? Яке питання чи ускладнення? Його треба записати, щоб не загубити. Далі розвʼязую питання будь-яким доступним способом. (Можна тут згадати про “стратегії навскіс” - це набір “порад” для розблокування творчих задач.) Але взагалі, в типовій ситуації немає великої проблеми в тому щоб просуватися далі; головне це залишатися сфокусованим на роботі.
Звісно, всіх ситуацій Screen Time не покриє, і так само треба стежити, коли хочеться сходити за кавою, або перевірити телефон, або робити ще щось, окрім поставленої задачі.
Тобто моя нова стратегія така: помічаю, коли відвернув увагу від роботи — записую причину — розвʼязую — рухаюсь далі. Помітити — ключове, та записати — теж ключове. Спробую тиждень та розкажу.

