Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
29.01.2024
Вбудовування бінарної бібліотеки в Ruby - словничок
Викликати бінарний код з Ruby - це настільки нормально, що стандартна бібліотека робить це щосекунди. На те є стандартний механізм інтеграції, який починається з ruby.h. Це заголовок C. На цей час С є спільною фундацією майже для всіх компільованих мов, тож будь то Go, Crystal, чи навіть асемблер, все одно буде інтегруватись через C.
Щоб зінтегруватись, ми повинні збудувати shared library, або ж як їх знають на Windows, DLL. При цьому ми підʼєднаємо до нашої бібліотеки “адаптер” до самого Ruby; на то є модуль mkmf. Сама бібліотека може бути написана будь-якою мовою, яка вміє створювати shared library.
Далі — у бібліотеці ми оголошуємо клас чи модуль: rb_define_module та наповнюємо його. А в Ruby - просто робимо бібліотеці require, та навіть нічого не треба знати про те, що вона зовнішня. Магія!
Втім, за досвідом, саме код інтеграції, який ховається за цією магією, буде найнеприємнішим. По-перше, він має бути написаний на C (або тонкій абстракції над C - такій, як CGO). По-друге, і це ще не все, бо доведеться конвертувати типи в спеціальні для Ruby та робити інші цікаві речі.
Щоб уникнути такого коду, я знаю два інструменти. SWIG - це генератор коду інтеграції з заголовків C. Тобто достатньо зробити звичайну бібліотеку без інтеграції з Ruby. Або навіть взяти чужу - 14 років тому я так загортав glew - бібліотеку доповнень до OpenGL: SWIG особливо гарний для інтеграції великих чужих бібліотек.
А FFI то вже модуль для Ruby, який здатний зінтегрувати готову динамічну бібліотеку. Робить він це через власний шар C, який ми метапрограмуємо з Ruby. Тому FFI буде повільніше, ніж SWIG чи пряма інтеграція. Хоча це буде помітно тільки на коротких та частих викликах. FFI має сенс брати там, де лізти в вихідний код бібліотеки не можна або не хочеться.
28.01.2024
Як я обрав Go, а не Rust
Go та Rust - багато в чому мови-конкуренти. Я жартую, що існує закон всесвіту, що для кожної програми на Go існує еквівалент на Rust. Наприклад, бачив заміни для ElasticSearch: Zinc та Sonic. Або, як вже писав, Wails та Tauri як альтернативи Electron.
Чисто абстрактно, я мав би бути фанатом Rust: чітка система типів, відсутність GC, дженерики з самого початку. Але ні — я вже восьмий рік працюю з Go, дедалі більше, а до Rust так і не дійшов. На то є історія.
В далекому 2016 році в мене був прототип рішення на Ruby, який ганяв багато даних через Redis та вже використовував вбудовану в Redis мову Lua, щоб робити частину обчислень прямо в базі. Для прототипу це було круто, проте для кінцевого продукту надто ускладнено. Вирішив зробити свою in-memory БД, яка і масиви даних тримає, і обчислення вміє робити.
На той час з системних мов програмування я знав тільки C/C++, та й те без досвіду розробки продуктів. На слуху були… Go та Rust. В цьому плані з 8 років нічого не змінилось (та й добре.) Головною задачею в мене було відтворити логіку, та зробити це за лічені дні, бо строки вже тиснули. Для перевірки обрав реалізацію невеличкого модуля, центрального для бізнес-логіки.
Спочатку я спробував Rust (бо, я ж кажу, він мені більше подобається.) Та швидко зрозумів, що на опанування системи позик (borrow) в Rust піде багато часу. Причому без неї навіть простої програми не побудуєш; займи не можна відкласти на майбутнє, як частини перевірок TypeScript. Це логічно, бо без позик Rust просто не знає, коли звільняти памʼять.
Тому перейшов до Go. В Go є найкращий інтерактивний підручник, який минулого року навіть переклали українською. З підручником я за годину зрозумів достатньо мови, щоб зробити свій тестовий модуль… та переконатись, що я зможу рухатись далі. Безперечно, Go не надає таких гарантій, як Rust, та потребує GC… але швидкість навчання була гідним відшкодуванням за це.
Так, за півдня я обрав Go, а не Rust. Проєкт був успішним. Та, річ у тім. що коли вже програмуєш на Go, то Rust виглядає як крок убік… крок, який досі в мене немає ніяких підстав робити.
27.01.2024
Тобі потрібна історія буфера обміну
У всіх свої інструменти та підходи до роботи за компʼютером. Коли працюєш в парі, можна багато дізнатись нового для себе. Однак є одна техніка, за відсутністю якої мене починає просто сіпати. Це історія буфера обміну.
Історія буфера обміну — це попросту, можливість вставити не останнє скопійоване значення, а одне з попередніх. Існує багато утиліт, які надають історію. Я просто користуюся вбудованою функціональністю Alfred. Вона привʼязана на комбінацію Hyper+V; тобто використання історії майже не відрізняється від звичайної вставки.
Згадай про це наступного разу, як повертатимешся до місця, яке тільки-но копіював, але вже затер наступним, а тепер воно потрібно знову. Причому так роблять й не по одному колу! Історія робить відтворення попереднього буфера тривіальним.
А ще не раз доводилося в історії знаходити значення, скопійовані набагато раніше; в Alfred можна зберігати значення до трьох місяців. Наприклад, під час дослідження скопіював шлях до файлу чи цікавий ID, а записати забув.
На налаштування історії піде не більше декількох хвилин, а далі вона буде готова тоді, коли знадобиться.
26.01.2024
Функція історії в VS Code
🗑️✨ Пост подяки локальній історії в VS Code, також відомої як Timeline. Шукай її на тій самій вкладці бічної панелі, що й каталог файлів.
Вона існує ще з 2020 та, на перший погляд, зайва, коли є Git. Але ж локальна історія повністю автоматично фіксує кожну збережену версію файлу. Тому вона корисна як резервна копія.
Сьогодні як раз трапилась така халепа, після якої я дійсно поважаю локальну історію. Файл я не просто відредагував, а видалив за помилкою. Причому як раз під час підготовки коміту. До того ж якщо файл видалити не напряму, а “відкинути зміни” в новоствореному файлі, то він не потрапить в смітник, а буде “НАЗАВЖДИ ВТРАЧЕНИЙ”, як попереджає VS Code.
От в такій ситуації я й згадав про локальну історію. Втім, вона працює на рівні файлу, а файл вже втрачений… Здогадався створити файл наново. Та, о диво! Таймлайн цього порожнього файлу містив всі попередні версії. Я вмить повернувся до останньої версії та абсолютно нічого не втратив.
Тепер локальна історія займає місце в моєму арсеналі, поруч з історією буфера обміну (про який мені ще треба написати) та іншими механізованими помічниками.
PS. Знаю ще аналогічну утиліту Dura, яка автоматично зберігає всі зміни в Git, незалежно від редактора.
25.01.2024
Генеративне тестування
Є такий різновид тестування, як генеративне, або ж ще кажуть тестування на властивості. Я такі на практиці бачу досить рідко, втім, корисно знати про цей підхід.
Типовий (не генеративний) тест утворює передбачуване середовище та перевіряє отримання конкретного результату. Причому ми завжди перевіряємо обмежений набір прикладів — таких, які, на нашу думку покривають поведінку коду. Це добре та приємно, втім, в деяких випадках перебрати всі значущі приклади не виглядає можливим.
Тоді й буде корисним генеративне тестування. Тут на вхід тесту подаються випадково згенеровані дані, а на виході перевіряються загальні властивості результату. Зазвичай генеративний тест запускається багато разів, за чим ми робимо висновок, що код працює без помилок та з очікуваним результатом.
Мені це згодилося для перевірки статистики, що генерується складним запитом в базі даних. Для перевірки я обрав таку властивість результату, яку також легко обчислити на Ruby з вхідних даних. Потім генерую дані, записую в базу, витягую з бази результат та порівнюю з тим, що обчислює тестовий код на Ruby. Даних багато, комбінацій ще більше, але код перевірки можна прочитати та зрозуміти — в цьому сила генеративних тестів.
24.01.2024
Ruby: ключові слова з опущеним аргументом
Вже два роки, як в Ruby є можливість опустити значення іменованого аргументу або ключа в хеші, якщо вона збігається з самим ключем. Тобто з версії 3.1. Мене то довгий час обминало, бо на Ruby я пишу нечасто, а синтаксис у цієї фічі трохи дивакуватий:
query = 'SELECT * FROM foos'
option = {timeout: 10, username:, password:}
adapter.execute(query:, options:)
Оці двокрапки мені особисто виглядають, як помилка. Особливо в Ruby, який славиться своїм акуратним та мінімалістичним синтаксисом. Втім, без двокрапки їх було б не відрізнити від нейменованих аргументів, це зрозуміло. В JavaScript, наприклад, підхід простіше, бо там іменовані аргументи — це ті ж самі обʼєкти, та неоднозначності немає (ну, поки ми не хочемо повернути обʼєкт з функції-стрілочки: x => {x} - тоді JavaScript переплутає обʼєкт та тіло функції.)
А щодо Ruby, то я знайшов що такі короткі іменовані аргументи — це чудова заміна позиційним аргументам всередині власного коду. З іменованими аргументами код стає надійніше, бо більше шанс на автоматичне виявлення помилок при рефакторингу.
def run_logic(a, b)
# переплутали порядок та не помітили
run_logic(b,a)
# змінили аргумент та не помітили
run_logic(a,c)
def run_logic(a:, b:)
# порядок не має значення
run_logic(b:,a:)
# помилка: немає такого аргументу
run_logic(a:, c:)
Ясно, що для того локальні змінні повинні мати ті ж назви, що й аргументи, втім так часто само собою буває всередині модуля.
23.01.2024
Opensearch DSL в Ruby, та чому він мені не подобається
У Opensearch для Ruby є DSL, який, звісно, запозичений в ElasticSearch DSL. Чим довше ним користуюсь, тим менше він мені подобається.
Може я не правий, але цей DSL для мене це приклад “DSL задля DSL”. По суті, він повторює те ж саме, що можна зробити побудовою JSON-у, тільки замість масивів та хешів отримуємо блоки. Зате, бачите, DSL придумали такий, що блоки не передають контекстну змінну всередину — все “красиво”, команди пишуться “чистими”:
definition = search do
query do
match title: 'test'
end
end
Тільки магії не існує; відсутність явного контексту значить, що він передається прихованим. По-перше, код реалізації від того дуже складний. По-друге, через набуття контексту з DSL код всередині блоку втрачає батьківський контекст та вже не може викликати методи з власного класу. Від того рефакторити код з цим DSL дуже боляче.
(До речі, щоб отримати доступ до self всередині блоку, можна призначити його звичайній локальній змінній: me = self. Локальні змінні мають локальну область застосування, їх в нас не відібрати!)
Якщо порівнювати з найуспішнішим DSL для Ruby - Arel - то в ньому завжди контекст (тобто запит в процесі побудови) є явним; його можна зберігати в змінну, передавати аргументом, використовувати багато разів. А не оце все.
22.01.2024
Мої думки про Baldur's Gate 3
Larian Studios навчились робити неперевершені тактичні покрокові бої. Ще у Divinity: Original Sin я побачив глибину рішень, якої просто ніде більше немає. Baldur’s Gate 3, за великим рахунком, успадковує всю базу тієї системи, та тільки фасад перетворює на лад D&D.
Бойова система Larian унікальна через синергію можливостей. Елементальні атаки плюс фактор оточення плюс різні типи уражень (площина, стріла) плюс розташування ворогів плюс послідовність кроків… гра дозволяє побудувати такі багатокрокові тактики, що відчуваєш що зробив неможливе. Та й баланс гри такий, що це стає потрібним. Боїв не так багато, зате кожний має свою арену, свої обставини. Таких, де всі тупо бʼють по черзі, просто немає, і це чудово.
В Baldur’s Gate 3 поки моя улюблена тактика — пхати. Бо це додаткова дія; вона доступна вже після того, як герой пересунувся та атакував. А гарним поштовхом можна легко відправити супротивника летіти з даху чи скелі.
В іграх від Bioware ніколи такої глибини не було — максимум це стратегічно розташувати героїв та підготуватися до бійки. Тому, якщо тобі бої в Baldur’s Gate 3 сподобались, раджу пограти ще в Divinity: Original Sin 1 та 2. Раніші ігри від Larian зовсім інші. До речі, Divine Divinity - досі дуже цікава гра з епохи Diablo 1, тільки з надзвичайно фізично інтерактивним світом — навіть на цей час.
Що не подобається — це скільки в діалогах шансових подій. Мене це тільки спонукає частіше зберігати гру. Може це й “схоже на настільну гру”, але в справжній D&D ведучий знайде, як після невдалої перевірки повернути мене до цікавого змісту, а в компʼютерній просто відчуваєш втрату… та перезавантажуєш.
21.01.2024
HomeAssistant та керування через ІЧ
Пару років тому хотів автоматизувати кондиціонери, щоб вони вмикались за датчиками температури — в тому числі й на режим обігріву. Кондиціонери розумом не відрізняються та мають звичайні пульти.
Я спочатку шукав якусь хитру шину комунікації та, відповідно, пристрій, яким можна додати до кондиціонера “розумність”. Виявилося все набагато простіше. В кондиціонері з пультом пульт зберігає всі налаштування, та відсилає їх в кондиціонер пачкою інфрачервоних сигналів. Тож, за наявністю ІЧ передавача можна легко запускати будь-який режим та навіть без зворотного каналу комунікації з кондиціонером. (Насправді в пульту теж немає зворотного каналу, тож він прекрасно “працює” навіть коли поруч немає кондиціонерів — просто оновлює свій внутрішній стан та відправляє ІЧ-команди.)
Далі, як підʼєднати до HomeAssistant ІЧ передавач? В мене вийшло з ще одним розумним пристроєм - Broadlink Universal Remote. Це практично шлюз між Wi-Fi та ІЧ. Залишається змонтувати цю коробочку в місці де вона бачить кондиціонер — та додати в HomeAssistant.
Тепер, про інфрачервоні команди. Кожна команда це послідовність сигналів. Щоб відправляти команди, особливо такі складні, як стан кондиціонера, потрібно їх знати. В теорії, є готові бази команд — можна знайти на GitHub - але, як на мене, легше записати їх власноруч. Як — написано в інструкції по інтеграції. Доведеться трохи підредагувати файли конфігурації, але потім просто направляєш справжній пульт на Broadlink, натискаєш кнопку та команда зберігається.
Перевага HomeAssistant в тому, що він здатний повʼязати абсолютно різні пристрої, та ще й зі складною логікою, та ще й в дружній для інженера формі.
Отак. А простіші обігрівачі можна контролювати через розумну розетку, тут ніяких пультів не потрібно. Тільки підібрати розетку, щоб по потужності підходила. На кшталт такої.
20.01.2024
Як в iOS застосунок керує відтворенням звуків
Таємний проєкт застосунку iOS про відтворення картинок отримав трошки уваги. В результаті я дещо вивчив про аудіомодель iOS та виправив неприємний момент UX.
Річ у тім, що я відтворюю GIF через відеопрогравач. Вбудованого, хорошого способу відтворення GIF взагалі немає. Зрозуміло, що GIF це половина мемів та іншого контенту, тож застосунок повинний не просто показувати GIF, а ще й робити це зручно — з перемоткою та іншим. Найпростіший спосіб цього досягнути — конвертувати GIF у MP4 та відтворити звичайним програвачем.
Але виникла проблема: програвач, навіть без аудіодоріжки, зупиняв відтворення будь-якого іншого аудіо на пристрої: музики, подкасту і таке інше. Хотілося б дивитися GIFки під улюблений подкаст, тож, що з цим можна зробити?
Як видається, в iOS для того є низка режимів відтворення аудіо, які конфігуруються модулем AVAudioSession. Цей модуль тільки надає системі метаінформацію про характер аудіо, який потрібний нашому застосунку. Саме відтворення працює й без нього — через програвач або іншим шляхом. Тому, думаю, коли зустрічаєш застосунок, який погано поводиться з аудіо, то автори просто не знали про AVAudioSession, як і я.
Для мого випадку, підійшла категорія відтворення ambient: вона значить, що звук з мого застосунку можна змішувати з іншими програмами. Інші режими вказують, чи треба ставити відтворення на паузу при замкненому екрані, чи впливає на нього тихий режим, і так далі. Ніколи не думав, що з аудіо все настільки неоднозначно.

