Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS
📢 Канал в Telegram @stendap_sogodni
🦣 @stendap_sogodni@shevtsov.me в Федиверсі

11.03.2023

Прості середовища для написання ігор

Зараз разом з сином шукаємо, в якому середовищі робити гру, або ж принаймні знайшли GameMaker, а тепер шукаємо гарний підручник для нього, поки без успіху.

Це наштовхнуло мене на думку: коли я вчився програмувати - а ігри були одним з того що я робив на початку - то моїм “гейм мейкером” був MS DOS та Borland Pascal. Простота цього середовища дозволила навіть без досвіду робити доволі складні проєкти. Причому це було в 2001 році, коли час DOS давно минув, та всі вже грали в Counter Strike. Але якби мені в той час дали OpenGL або DirectX, навряд чи я опанував би їх так же ж, як і SetColor або LineTo з графічної бібліотеки Borland Pascal.

А зараз, коли роблять гру на кшталт Undertale, то беруть Game Maker. Хоча мені особисто хотілося б чогось це простішого, але орієнтованого на програмістів. От ще давно цікавить PICO-8 - це “віртуальна консоль”, тобто фактично середовище програмування - суворо обмежене в можливостях, та в цьому схоже на мої часи з Паскалем. До речі, пишуть для PICO-8 на Lua - про цю мову варто знати, бо вона розроблена для вбудовування в інші програми - компактна та безпечна.


10.03.2023

Як всиновлюють бібліотеки

Коли ми почали розробляти бібліотеки для інтеграції з нашим продуктом, виявилося, що наше брендове імʼя було зайняте. Я зголосився домовитись про передачу прав, та тому у нас зʼявився гем mailtrap та пакет NPM mailtrap. Замість того, щоб це були якісь mailtrap-sdk. Що в ізоляції, може, й прийнятне, але поруч з обманливим пакетом mailtrap створюватиме зайве непорозуміння.

Як це робиться. Знаходиш контакти автора, та пишеш листа. Звісно, для цього пакет має бути без активної підтримки. В нашому випадку минулий гем був з 2007 року та, здається, містив посилання на Rubyforge (в кого досвід по рубі 15 років, той памʼятає.) NPM-пакет був свіжіший, але все одно заархівований автором 2 роки тому. Втім, для пакета я додав у README посилання про те, що трапилось та де знайти старі версії. Загалом, для передачі прав достатньо було ввічливо попросити.

А от з Python все цікавіше, бо є цілий протокол передачі прав PEP-541. (Для мене це якийсь новий рівень бюрократії.) Треба зробити пул-реквест в особливий репозиторій за всіма правилами. Іронія в тому, що потім 3 тижні ніхто з команди на нього не відповідає. Непогана ілюстрація до принципу Agile “люди понад процеси” - замість довжелезної документації краще б була відповідальна команда підтримки. (Можна, звісно, домовитись з автором напряму.)

Почесна згадка - Go, бо там кожний пакет має URL. Тож конфліктів імен не може виникнути, та коли будемо робити свій, то це буде просто github.com/railsware/maltrap-go.


09.03.2023

Merlin Bird ID - Pokemon Go, але для птахів

🦜 Знаєте Pokemon Go? Так от, минулого року я дізнався про додаток, який вміє впізнавати птахів за співом - Merlin Bird ID. Для мене цей додаток перетворився на “реальну” версію Pokemon Go: почув спів незнайомої пташки, я вмикав розпізнання співу, та, якщо пощастило, “ловив” птаха у свій каталог. Розпізнання працює приблизно як з музикою, тобто будується спектрограма наявного звуку, та порівнюється з базою даних співів. Я ні в якому разі не орнітолог, та й взагалі, на початку мої знання обмежувались банальними горобцями та сороками. Але, Merlin працює так гарно, що за минулий сезон я дізнався чимало видів пернатої фауни.

Власне, ще з осені чекаю, щоб про це написати, бо взимку птахи не співають. А от зараз, коли повернулась весна, час виходити на аудіополювання. Не обовʼязково для цього ходити в ліс, птахів можна знайти й в парках та скверах, тож раджу звернути на це увагу на наступній прогулянці.

З птахів, що я “спіймав”, найцікавіший — вивільга. Хоч ця пташка і яскраво-жовтого кольору, але я ніколи їх не помічав, хоча її характерний спів знайомий мені з дитинства. Та цікаво, що невдовзі після того, як я впізнав вивільгу програмою, то й побачити теж довелося — мабуть, бо знав, що шукати.

Також: виявив, що на Дніпрі мешкають не чайки, а крячки; як співає одуд (бо їх раніше навпаки, тільки бачив), та окрім всього того розширив словниковий запас англійської, бо Merlin видає результати саме цією мовою.


08.03.2023

Відтворення стану бази для тестів з FactoryBot.

Найцікавіше, що я робив за останні дні — то розбудував цілу систему для відтворення стану бази для тестів. Трохи вже писав про це в грудні. Але то був прототип. Покористувавшись ним, побачив, що можна було зробити краще. Оскільки кодом проєкту поділитись не можу, розкажу загальні ідеї.

По-перше, у FactoryBot багато нетривіальних функцій, знаючи які, можна переносити більше логіки створення записів у фабрики. В ідеалі, всі неважливі для тесту атрибути мають бути приховані в фабриці. В цьому допомагають, наприклад, тимчасові атрибути, створення декількох обʼєктів з блоком налаштувань, та, звісно, риси. На кожний семантичний різновид запису для тестів краще робити рису, саме так й будуть приховані неважливі атрибути.

По-друге, можна не зупинятись на фабриках, а загортати їх в більш доцільний DSL. Якщо ваша бізнес-логіка потребує особливого стану бази, то можна сховати подробиці за DSL. Це може бути як власний модуль, так і складна фабрика, що створює не просто запис, а цілий контекст. Завдяки колбекам фабрики можуть робити буквально будь-що.

Головна мета — тест має бути написаний мовою бізнесу. Бо найгірше для тестів — це коли треба підготувати багато контексту, але зміст цього контексту втрачається за сервісним кодом, таким як виклик фабрик та оголошення параметрів.

Як приклад: якщо в моделі інтернет-крамниці є користувач, а в нього є крамниця, а в ній є товари, та ми пишемо тест для модуля, що обробляє товари, то він не має явно описувати користувача та крамницю. Краще мати фабрику, яка разом з товаром створює його необхідні асоціації. Або якщо зайти з іншого боку, фабрика може створювати крамницю з наповненням товарами. Але головне, що неважливі, хоч і потрібні для тесту записи мають бути прихованими з коду самого тесту.


07.03.2023

Пошук помилок методом виключення всього зайвого

При пошуку причини помилки часто радять міняти обережно по одному фактору, а потім повертати все в початковий стан та міняти щось інше. В цілому, це слушна порада, але що якщо система велика, та потенційних факторів неосяжно багато?

Пропоную ще один підхід. Почати видаляти з системи частини, починаючи з найдальшої від місця помилки. Помилка на бекенді? Викидаємо фронтенд. Потім викидаємо вебсервер. Запускаємо модуль в максимальної ізоляції, яку можна розумними витратами часу. Падає тест тільки на CI? Викидаємо зі сценарію CI все, окрім запуску цього тесту.

Далі — дивимося. Якщо помилка відтворюється — чудово, ми звузили поле пошуку. Якщо не відтворюється — значить, не вгадали з місцем-джерелом, пробуємо інше та починаємо спочатку. Якщо код поламався, але по-новому — то, напевно, багато вирізали, та треба дещо повернути.

Коли працюєш з кодом та з системою контролю версій, завжди можна відкотити зміни. Користуймося цим!


06.03.2023

Не можна перескочити на розвʼязок

Не можна відразу перескочити на розвʼязок. Втомився писати довгі пости, тому сьогодні короткий та філософський.

В мене є вада характеру — коли випадає складна задача, то я довго над нею думаю, замість того, щоб почати писати код. “Довго” може вимірюватись днями. В теорії, якщо гарно подумати, то можна знайти більш просте та красиве рішення. Якщо поки красивого рішення немає… треба подумати ще?

…Коли справа доходить до коду, то рішення неодмінно перетворюється, байдуже, що я там придумав. Як малюнок з голови не перенесеш на папір, так само й код в уяві тільки здається, що працює. Тому краще, на мою думку, якнайшвидше почати писати щось, навіть якщо поки це “щось” складне та неохайне. Поки руки пишуть код, голова врубається в контекст. Якщо й можна знайти справжній розвʼязок, то тільки так.


05.03.2023

Як Ruby, Node.js та Go витрачають памʼять на функції

Ні, не міг зупинитись без того, щоб розглянути також функції як значення. В Go та JavaScript це можливо напряму, а в Ruby є так звані лямбди (хоча це й не єдиний спосіб, як я вже писав, але найбільш поширений).

Власне, що значить — зберегти функцію? Код функції памʼяті не займає, принаймні, не в тому сенсі, як змінна. Та дійсно, у всіх мовах розмір функції ніяк не впливає на витрати памʼяті. В кожній мові посилання на функцію займає по-різному. В Go - чесні 8 байтів, в JavaScript - 70, в Ruby - цілих 241 байт. Знову відчувається різниця між системною мовою, мовою з функціональною парадигмою, та мовою, де лямбду нечасто побачиш.

Але на цьому витрати на функції не закінчуються. Можна подумати, що параметри функції щось займають, проте ні — параметри належать виклику функції, а не її визначенню. Але ж в усіх трьох мовах функції є замиканнями, та посилання на функцію зберігає доступні їй змінні. Чим більше змінних, тим більше памʼяті. Як завжди, Go окрім значень, нічого не зберігає, а в JS та Ruby є невеликі додаткові витрати.

Здивувало ось що. Які саме змінні включаються в замикання? Інтуїтивна відповідь — ті, що в ньому використовуються. На Go це абсолютно вірно — бо Go ретельно відстежує шлях використання змінних, та навіть не дасть скомпілювати програму, якщо деяка змінна залишається невикористаною. Зрозуміло, що Go чітко знає, що потрапить в замикання.

В Ruby все навпаки. В Ruby лямбда замикає всі доступні змінні — незалежно від того, використовуються вони в ній чи ні. Це важливо знати, бо всі ці змінні утримуватимуться в памʼяті, поки існує лямбда. Чому так? Я не впевнений, але здається, тому, що в Ruby є метод Binding#local_variable_get, яким можна за імʼям дізнатись значення будь-якої змінної. Та дійсно, цим методом можна підгледіти змінні поза межами лямбди:

def hidden_binding(i)
  j = i * i
  ->(name) { binding.local_variable_get(name) }
end
peek = hidden_binding(2)
puts peek.call(:j) # 4

Отакої! Гнучкість мови Ruby дається не безплатно.

В JavaScript такого не відбувається, якщо тільки в замиканні немає функції eval. Якщо є, то функція також захопить всі доступні змінні. Це показують і витрати памʼяті. Причому перевірка на eval відбувається статично, тобто якщо eval передати параметром, то замикання не буде повним:

function hiddenBinding(i) {
  let j = i * i;
  return (x) => eval(x);
}
console.log(hiddenBinding(2)("j")); // 4

function hiddenBinding2(i, f) {
  let j = i * i;
  return (x) => f(x);
}
console.log(hiddenBinding2(2, eval)("j")); // j is not defined

(Ясно, що порядні люди пишуть код без eval. Але он воно як цікаво працює!)


04.03.2023

Вимірюємо витрати памʼяті у JavaScript

Для закінчення моєї поверхневої серії про витрати памʼяті, розглянемо JavaScript. А точніше, інтерпретатор Node.JS, бо інтерпретаторів існує чимало, та поводитись вони можуть по-різному. Код все ще на gist.

В JavaScript є вбудована функція для отримання витрат памʼяті - process.memoryUsage(). А от для виклику збирача сміття треба запускати скрипт з ключем --expose-gc, інакше функція global.gc() не буде доступною.

Так само як і в Ruby та Go, числа в JavaScript займають 8 байтів, тобто зберігаються без обгорток. Так само масив чисел займає стільки памʼяті, скільки самі числа. Виникає питання — навіщо тоді в JavaScript є ще класи для типізованих масивів - Int8Array та інші? Знайшов пояснення. Головна причина в тому, що типізовані масиви відповідають єдиному блоку памʼяті, та їх можна передавати в усілякі API, які такий блок очікують. Тож не варто заміняти свої масиви на типізовані, якщо немає на них споживача — звичайні масиви не менш ефективні.

Константні рядки так само зберігаються одноразово. Але це не єдине, що тут незвично. Рядки у JavaScript представляються у кодуванні UTF-16, де “прості” символи займають по 2 байти. А памʼять під них Node.js виділяє блоками по 8 байтів, напевно, щоб рядок був кратним розрядності процесора. До речі, не писав про це окремо, але як в Ruby, так і в Go рядки зберігаються в “рідному” кодуванні, що для більшості сучасних програм значить UTF-8. Так що, для зберігання текстів латиницею програма на JavaScript споживатиме вдвічі більше памʼяті, ніж Ruby або Go. Але тут теж все не так просто, бо мені здається, що Node.JS стискає рядки, бо на рядки з повторюваними значеннями уходить менше памʼяті. Треба це детальніше роздивитись.

Зі структурами все теж цікаво. В JavaScript все є обʼєктом. Будь то асоціативний масив, клас, будь-що інше. Це підтверджують мої тести: обʼєкти, створені літералом, та такі, що створені конструктором, памʼяті займають однаково. Але з літералами не все так просто. Помітив, що додаткове чисельне поле в літералі збільшує витрати тільки на 8 байтів. Але ж де тоді зберігається ключ цього поля? Моя інтуїція каже, що в JavaScript структура обʼєкта зберігається окремо від значень, тому обʼєкти, створені одним та тим самим літералом містять тільки свої значення. Таким чином, JavaScript по використанню памʼяті точно краще, ніж Ruby, та має показувати витрати, схожі на Go. Дуже приємний висновок.

На цьому поки закінчую дослідження витрат памʼяті. Короткі висновки: для зберігання простих типів та масивів всі 3 мови підходять приблизно однаково; Ruby суттєво програє в витратах на структури; JavaScript навпаки, майже не гірше за системну мову; Go, звісно, буде оптимальним, якщо тільки не зберігати все в асоціативних масивах.


03.03.2023

Витрати памʼяті Ruby та Go, продовження: структури

Продовжую вчорашні тести. Доробив код, зробив зручну функцію для вимірювання, щоб прибрати копіпасту. На Go згодилися узагальнені типи; безумовно, корисна можливість, та ще й така що не погіршує швидкодію програми — про це далі. Сьогодні роздивимось структури. За тестовий приклад я обрав структуру з трьох чисельних значень; тоді розмір самих значень відомий - 24 байти, та можна підрахувати накладні витрати від самого механізму структури.

Починаючи з Ruby, найбільш очевидним здається клас Struct. Тут на кожний атрибут додаткові витрати ставлять 22 байти. Такі самі витрати будуть, якщо створити власний клас, без Struct. Тобто схоже, що Struct не оптимізує, а тільки спрощує код.

Хеш інтуїтивно буде менш ефективним. Тест це підтверджує: тут, на перший погляд, витрати аж в 3 рази більше. Але ось що цікаво: той самий хеш розміром у 224 байти спроможний зберігати до 8 значень без додаткового виділення памʼяті!. Схоже на те, що памʼять виділяється блоками, бо з девʼятого значення використання стрибає у 2 рази. Виходить, що для маленьких структур краще клас, а для великих можна не перейматись, бо хеш не додає зайвих витрат. Також, хеш, де ключі — це рядкові константи, важить так само як і хеш з символами. Ось скільки дивних нюансів такого простого, на перший погляд, типу.

Як щодо Go, то тут структури вбудовані в мову, та в традиції системних мов програмування, звичайна структура не створює жодних додаткових витрат — займає рівно стільки, скільки значення, які вона містить. Сподіваюсь, це пояснює, чому для потреб, де памʼять критична, беруть Go, а не Ruby.

Вказівник на структуру додасть свої очевидні 8 байтів. Більш цікаво, що перетворення типу в interface{} додає 9 байтів — тобто якщо використати конкретні типи, то вони не мають додаткових витрат, а інтерфейсний тип — має. Що підкреслює цінність впровадження узагальнених типів, бо як раз узагальнені типи працюють так само як еквівалентний код, де всі типи підставлені вручну.

Про асоціативні масиви в Go коротко скажу, що вони дуже схожі на хеші в Ruby. Таке саме виділення памʼяті блоками, та приблизно такі самі витрати на елемент — десь 24 байти. Різниця тільки в тому, що в Go структури набагато вигідніше в порівнянні. Наприклад, якщо використати тип map[string]interface{} для лінивої обробки JSON, то платити доведеться не тільки за масив, а ще й за інтерфейсний тип. Так що інтуїція по Go - якщо форма структури відома заздалегідь, завжди треба брати структури, а якщо ні — тільки тоді масиви.

Завтра — все то саме, тільки для JavaScript.


02.03.2023

Порівняння витрат памʼяті Ruby та Go на масиви

Хотів написати пост про те, як Go краще витрачає памʼять, ніж Ruby, та ще й з прикладами. Але вийшла несподіванка — не такий вже Ruby й поганий! На масив чисел якимсь чином витрачає десь стільки ж памʼяті (на 2% менше навіть!), а от на масив рядків вже на 50% більше, що більш зрозуміло. Код прикладів у gist. Тут треба більше розбиратись.

По-перше, що вимірювати як витрати пам’яті. На мою думку, краще за все дивитись на загальний розмір памʼяті, виділений процесом, тобто RSS - Resident Set Size. Ані в Ruby, ані в Go немає вбудованих засобів дізнатись цей розмір, але його можна легко отримати командою оболонки ps -o rss $PID_OF_PROGRAM. Корисно, що цей показник не спирається на особливості мови чи платформи, тому таке порівняння більш справедливе. Залишається тільки виділяти достатньо памʼяті. Та про всяк випадок викликати збирач сміття перед вимірюваннями; хоча в такому простому тесті це не має ніякого ефекту. Збирати сміття варто, якщо вимірювати витрати цілого алгоритму.

Так от, виходить, що як Go, так і Ruby зберігають масив цілих чисел у 64-бітному відображенні, та практично без додаткових витрат. Причому в Go кількість бітів у типі задається явно, а от від Ruby можна було очікувати й 32-бітних чисел, бо мої значення влазять в 32 біти. Втім, виглядає так, що Ruby завжди використовує 64-бітні числа, що логічно на моїй 64-бітній системі.

Рядки на Go витрачають, окрім байта на кожний символ, додатково 8 байтів на вказівник, та 8 байтів на довжину рядка (або так це виглядає). В Ruby, напевно, рядок містить ще якесь значення — можливо, кодування.

Можемо також підтвердити економію, яку Go отримує на тому, що рядки — є константи]. А саме, якщо масив наповнити не різними рядками, а одним й тим самим рядком, то витрати памʼяті будуть такі самі, як на масив чисел. І це незалежно від довжини цього рядка. Це пояснюється тим, що фактично масив містить вказівники на один та той самий рядок, а вказівник займає ті самі 8 байтів, як і число. В Ruby можна досягнути такої самої економії, якщо робити масив символів, тобто :symbol. Що зайвий раз підкреслює важливість використання символів там, де одне й те саме значення повторюватиметься багато разів — наприклад, при декодуванні JSON.

Але масиви — то надто просто. Цікавіше буде порівняти витрати на структури або хеші — фундамент сучасного програмування. Про це — наступного разу.