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

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

Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni

23.08.2024

Дизайн та естетики

Ділюся ще досі сирими, але впевненими міркуваннями про те, як зрозуміти дизайн, якщо пощастило бути програмістом.

Є таке модне сучасне слово: естетика. (Тобто слово старе, а значення надсучасне.) Естетика — це, моїми словами, сукупність відчуттів від якогось предмета. Є зміст, а є естетика. На відміну від інших схожих слів, естетика фіксує точку в просторі, навколо якої скупчуються схожі предмети. Є ціла енциклопедія естетик.

Естетики є в інтерʼєрі, одязі, вебсайтах… є вони й в коді. Наприклад “естетика Ruby on Rails” багато у свій час наробила хвиль. Естетику складно описати словами, вона невербальна, легше побачити. Тим важче формалізувати: правила на кшталт “роби функції не довше 10 рядків” корисні, але не навчать тебе програмувати зі стилем; це приходить з досвідом.

Так само й в дизайні, будь-який дизайн не створюється в вакуумі з голови, а існує в культурному просторі. Щоб навчитися робити щось гарне, треба знайти естетику, яка тобі до вподоби, збирати приклади та відтворювати їх. В запозиченні немає нічого ганебного: поки починаєш, твоя задача привʼязатися до свого орієнтира.

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


22.08.2024

Мова програмування С як основа

Я мимохідь сказав, що серед всіх мов С найважливіша для розуміння компʼютера. Так склалося, що у C особливе положення: це фундамент та спільна база для безлічі інших мов. (А C++ - це зовсім інша мова, а ніяк не просто “доповнення до С”, як може здатися!)

C з першого погляду здається схожим на будь-яку сучасну мову. Тут є функції, цикли, масиви. Структури! Проте все не так, як здається: кожний елемент мови C максимально наближений до машинної реалізації, тож замість зручності використання ми отримуємо близькість до заліза.

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

Структури — це теж просто блок памʼяті, тільки поділений не на елементи, а на поля; причому розташування поля в памʼяті настільки передбачувано, що структурами можна розбирати двійкові файли чи мережеві повідомлення. Це дуже просто: беремо вказівник на масив прочитаного буфера, та призначаємо його вказівнику на структуру потрібного типу. Ось вам і справжній zero-copy (тільки не забудьте не звільнити один з вказівників, бо буде біда!)

А ніякого ООП тут немає. Принцип виклику функцій теж найпростіший: передали аргументи, викликали, отримали результат. Настільки простий, що саме модель виклику C є lingua franca всіх мов: саме через неї Ruby може викликати бібліотеку, написану на Go чи Rust.

За проєкт для вивчення C я б порадив зробити гру або навіть демку. Причому взяти для цього найпростішу графічну бібліотеку raylib та веселитися, як у 1999-му! Або почати з читання вічної та епічної книги “Мова програмування C” Кернігана та Річі. `


21.08.2024

Високорівневі мови та навіщо вони потрібні

Насправді низькорівнева мова здатна виконати всі високорівневі функції: принаймні тому, що будь-яка високорівнева мова спирається на низькорівневу всередині. Що ставить запитання: а навіщо тоді всі ці високорівневі мови?

Я б хотів сказати, що вони розкривають якісь нові парадигми, але на практиці популярні високорівневі мови тримаються у фарватері “ООП + імперативний код + трохи функціонального”. А ті, що особливі, залишаються нішевими.

Тому натомість відзначу, що найголовнішим кроком є відхід від явної типізації. Багато коли “типи або без типів” є питанням смаку, але в одній області динамічна типізація суттєво полегшує життя. Це робота з часткою складної структури даних — наприклад — з чужим API чи з файлом експорту в JSON. Я не хочу писати типи — я тільки хочу отримати дані за шляхом! Взагалі існування формату JSON спирається на динамічну типізацію.

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

Знання високорівневої мови допоможе розвʼязувати задачі “на гнучкість” швидше. Це може бути як проста задача (наприклад, три дні тому, щоб згенерувати потрібний JSON, я написав один рядок Ruby, а ніяк не програму на Go), так і цілий проєкт (наприклад, “традиційні” вебдодатки, де Ruby залишається непереможеним). Не кажучи вже про галузі, де мова визначена за нас — наприклад, JavaScript в браузері чи Python для аналізу даних.


20.08.2024

Низькорівневі мови та навіщо вони потрібні

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

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

Щоб розуміти цю ціну, напевно, спочатку треба ознайомитися якщо не з асемблером, то принаймні з C та принципами його роботи. Бо тоді стає зрозуміло, чого не може зробити високорівнева мова.

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

Низькорівневі мови дозволяють позбавитись цього гандикапу — інколи якісно суттєвого, ціною наближення до машинного коду. Процедурного стиля програмування, статичної типізації (тобто оголошення розміру значень заздалегідь), більш явного керування памʼяттю тощо.

Та, навіть коли твоя головна мова — високорівнева, варто знати деяку низькорівневу для тих випадків, коли потрібно витиснути з системи все. А також, можливо, щоб розуміти, як воно все працює всередині. Чи буде це С, С++, Rust, Go чи D - вже менш важливо.


19.08.2024

XcodeGen: генератор для проєктів XCode

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

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

Тому приємно було дізнатися з сусіднього блогу про існування утиліти XcodeGen, яка просто бере та генерує всю конфігурацію проєкту з “рукописного” YAML. А файлову структуру зчитує з диска. Таке я люблю!

Єдине, що складно, це переносити проєкти, які вже існують, бо як я вже казав, не так легко розібратися, що з їхньої конфігурації змістовно. Та й на жаль перша генерація повністю перегортає весь project.pbxproj, тож не легше зʼясувати, що залишилося за бортом. Але мало-помалу переїхати вдасться, а далі конфігурація вже буде стабільною.


18.08.2024

Словники в Go: безплатних сніданків не існує

В продовження історії з модулем unique для Go 1.23, хотів на практиці перевірити різницю між використанням символу та рядка.

Вимірював витрати памʼяті map[unique.Handle[string]]int проти map[string]int. Щоб ключ повторювався, наробив JSON з мільйоном обʼєктів {"foo": 123}. Дійсно побачив різницю: 204 Мб проти 289 Мб… тобто 85 байтів на економії на елемент. По-перше, 85 байтів це якось незрівнянно багато. По друге… чому взагалі той масив займає в памʼяті 200 Мб? Це у два рази більше, ніж текст JSON.

Почав дивитися, як зміняться витрати, якщо зробити більш ніж один ключ. А ніяк! Виявилося, що Go резервує в новому словнику місце під 8 записів. Тоді вже ясніше: насправді надлишок на ключ-рядок проти ключа-вказівника лише 11 байтів: вказівник займає в памʼяті 8 байтів, а рядок - 16 байтів, плюс 3 байти на сам зміст. (Тобто короткі рядки теж неочікувано неефективні.)

Що, взагалі, меркне в порівнянні з базовими витратами у 200 байтів на кожний малесенький словничок. Мораль історії така: низькорівнева мова не робить програму чарівно швидкою та ефективною. Роздуті структури даних та вкладені цикли можна писати будь-якою мовою.

Звісно, в Go можна зробити набагато краще: оголосити структуру. Скільки займає struct { Foo int }? 8 байтів. Але я не завжди звертав увагу на місця, де словники можна було замінити структурами. Особливо важко, коли паралельно програмуєш на мові, де між ними практично немає різниці. Таке.


17.08.2024

Модуль unique в Golang 1.23: глобально-унікальні символи

Не встиг я подати ідею про заміну повторюваних рядків на глобально-унікальні символи (interning), як вийшов Go 1.23, де зʼявився модуль unique, який буквально те й робить. Приємна несподіванка!

Метод unique.Make підтримує не тільки рядки, а й будь-який порівнюваний тип, та замінює значення на глобально унікальні вказівники. Рядки-літерали в Go і так є константами, хіба що кожна копія має власний 16-байтовий заголовок. А от якщо доводиться отримувати багато повторюваних рядків ззовні, модуль unique стане до нагоди.

Наприклад, ми завантажуємо з бази фічафлаґи для поточного користувача. Кожний виклик утворює рядки наново та залишає за собою купу сміття. Якщо зробити їх типом unique.Handle[string], то ловимо двох зайців: фічафлаґи займають менше памʼяті, але до того ж тепер їхнє порівняння зводиться до порівняння вказівників. Виходить практично те ж саме, що замінити фічафлаґи на чисельні константи (доволі очевидна оптимізація), тільки повністю автоматично.

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

До речі, для того використовуються слабкі вказівники — тип weak.Pointer. То є вказівники, які не впливають на збирання сміття. Тобто все, що робить функція очищення — це чекає, поки значення звільнить прибиральник, після чого видаляє вивільнені значення зі словника.


16.08.2024

Навіть прості справи варто записувати

Пʼятничний пост про GTD. Те, що варто не тримати все в голові, а краще записувати, це зрозуміло. Але… може хоч памʼятати “що зробити” це корисно? Цього тижня зловив розуміння, що як раз ні, навпаки.

Задача була така: “зателефонувати виконробу”. Ходив я з нею декілька днів. Скільки не згадував, так і не знайшов нагоди зателефонувати. Можна було б пояснити це прокрастинацією, тобто простими словами, лінню.

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

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

Це і є те, про що йдеться в GTD: не тримай справи в голові, записуй. Навіть якщо справа не велика — зовсім не обовʼязково її можна “взяти та зробити”.


15.08.2024

Cap'n Proto - серіалізація без копіювання

Роздивлявся формат серіалізації Cap’n Proto. Його особливістю є відсутність копіювання даних. Це значить, що коли у вас є буфер даних (та схема до нього), то значення можна забирати прямо з буфера. Фактично Cap’n Proto не стільки серіалізатор, скільки формат організації даних у масиві памʼяті.

Схожих рішень не так багато. Можу відзначити flatbuffers від Google як справжню альтернативу. Тут треба обовʼязково роздивлятися, як воно підтримує вашу платформу (в моєму випадку - Go.) Взагалі я б сказав, що рідко трапляються обставини, щоб саме серіалізація була вузьким місцем.

Проте мені подобається те, що з масивом даних можна працювати по шматочках. Наприклад, якщо у вас є JSON в 1 Гб, то доведеться весь його завантажити та розібрати — навіть коли з нього потрібне одне значення. А якщо Cap’n Proto, то можна прицільно забирати конкретні записи та значення. Це гарно працює з mmap.

Також за моїми вимірами масив даних Cap’n Proto займає суттєво менше памʼяті, ніж еквівалентні структури Go. Особливо по витратах на керування памʼяттю. Там, де у звичайних структур спостерігаю надлишок майже в 100%, у Cap’n Proto його практично немає. Та й не дивно, бо в памʼяті утримується єдиний звичайнісінький масив байтів.

З незручного: Cap’n Proto не підтримує зміну довжини масивів (без перестворення). Також відсутній тип-словник. Словників взагалі в zero-copy не бачу — ймовірно, тому, що для ефективної роботи їх доведеться пересортовувати. Але словник завжди можна замінити пошуком за відсортованим списком.

Наразі автор Cap’n Proto працює у Cloudflare, а сам формат широко використовується у Cloudflare Workers, зокрема для зберігання даних.


14.08.2024

Gzip та Base64

Коли потрібно надати двійкові дані в стислій текстовій формі, варто спочатку їх стиснути, а потім кодувати у Base64. Або, як наслідок, навіть якщо на сервері є автоматичне стискання на виході, краще все одно стиснути вхідні дані.

Чому так? Бо суть алгоритму Gzip - побудова словника з повторюваних послідовностей. (Алгоритмів взагалі є декілька, але зазвичай береться Лемпель-Зів-Велч, про нього й мова.) Повторювана послідовність залишиться такою в Base64 тільки тоді, коли вона вирівняна відносно “сітки” по три байти: Base64 кожні три байти перетворює в чотири символи. Ба більше, кожний випадок послідовності мусить бути вирівняний однаково: бо той самий байт на 1, 2, 3 місці в трійці буде представлений в Base64 абсолютно по-різному:

"foo" => Zm9v
" foo" => IGZvbw==
"  foo" => ICBmb28=
"   foo" => ICAgZm9v

Бачите, як послідовність Zm9v зʼявляється знову тільки там, де перед нею три пробіли? На практиці Gzip зможе замінити лише ту частину повторів, якій пощастило збігтися з сіткою. Та ще й обрізаних під неї.

А Base64 навпаки, все одно, що кодувати. Ефективність тут стала: хоч сирі дані, хоч стиснуті, хоч білий шум — все збільшиться в розмірі на 33%.