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

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

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

28.08.2024

Структурне мислення та мапи думок

Приблизно відтоді, як відкрив для себе Obsidian Canvas, в моєму житті відбулася просто революція когнітивних мап. Це прям помітно розширює здібності.

Я взагалі звик до текстових документів чи списків-нарисів. Мапа мені зручніше, бо вона не є лінійною. Пробував після мап робити нариси — вони скоріше втрачають ясність та стають стіною тексту, який потрібно читати, щоб знайти місце. А в мапі місце часто запамʼятовується візуально. Так би мовити, складність O(1) замість O(N).

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

По-перше, не доводиться тримати в голові весь “стек” того, про що можна подумати. Так голова вільніша думати “вперед”, а не “назад”. По-друге, ще й нічого не забудеш. Мапа залишається як нагадування про всі можливі напрямки міркувань.

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

Я зазвичай бачу приклади мап думок як щось завершене, як конспект. Треба розуміти, що найбільше користі від них в процесі; тому всім раджу спробувати власноруч. Софт шукати не обовʼязково — вистачить аркушу A4.


27.08.2024

Сповіщення про закінчення задачі

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

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

Команда say в MacOS вміє промовити голосом буквально будь-який текст. Хоч статичне, хоч say "$?", щоб дізнатися код результату, хоч взагалі промовити весь вихід з нашої задачі: make test | say.

Якщо копнути ще глибше, то стандартні термінали “посигналять”, якщо їм зустрінеться символ BEL. Його можна отримати різним чином, наприклад, tput bel або echo \7. Хоча сьогодні помітив, що VSCode так зробить тільки якщо увімкнути відповідну опцію accessibility.signals.terminalBell. Ну, та й сигнал менш помітний, ніж say.

Також є цікаве рішення на той випадок, якщо команда вже запущена, займає більше часу, ніж очікувалось, та ти хочеш відійти, але дізнатись про завершення. Тут треба відправити задачу в фон традиційною комбінацією Ctrl+Z, а потім повернути з фону командою fg з додаванням потрібного “хвоста”: fg; say "all done".


26.08.2024

Випадково невдалі тести

Що ви робите з випадково невдалими тестами? Роки йдуть, а ця неприємність залишається поруч. Хоча причина, принаймні в мене, змінилася. Колись головною проблемою були побічні ефекти та взаємовплив тестів. Через те досі в Ruby запускаємо тести у випадковому порядку. Але, сучасні тести дають хибу через асинхронні події в самому тесті. Головним чином це тільки інтеграційні тести. З одного боку, їх мало, що гарно; з іншого — вони довгі. Що погано.

Раніше я був тієї думки що випадково невдалі тести потрібно збирати, відстежувати частоту та виправляти за критичністю. Зараз бачу, що такі списки застарівають. Натомість можна просто відкрити перелік останніх невдалих запусків CI, та виправляти просто по черзі. (В GitHub Actions, наприклад, можна відфільтрувати запуски за статусом та гілкою.)

Далі беру тест та запускаю його по колу локально, скриптом на кшталт такого:

for i in (seq 0 100000); echo $i; runtest; or break; end

(Це мова оболонки fish, якщо що.) Так набагато зручніше ніж запускати повторно вручну. Можна поставити та займатись чимось іншим. А якщо тест не падає 100, 1000 разів, можна зробити висновок, що він не такий вже й невдалий. (Тому немає великого сенсу заздалегідь вести статистику.)

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

Залишається знайти причину та виправити… Хоча це й нелегко, але все ж чомусь найскладніше це досягти відтворення тесту з помилкою.


25.08.2024

Функціональні структури та Redux

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

А насправді функціональні структури сяють там, де змінюються поступово та частково. З моїх задач в таке означення вписується Redux - бібліотека функціонального стану для React (та не тільки). Тут дійсно є і велика структура даних, і поступові та дуже ізольовані її зміни.

Ще кориснішою незмінність буде при застосуванні Reselect, бо там ефективність спирається саме на рівність вхідних даних за значенням, а бібліотека Immutable як раз реалізує таку рівність для власних структур. (Бо без неї треба чимало робити самотужки, я про це колись писав статтю). Тобто тут ми вже заощаджуємо на тому, що робимо менше обчислень завдяки мемоїзації.

Залишається питання — чи пришвидшить Immutable роботу застосунку з Redux? Треба буде перевірити — це вже дійсно практична ситуація. (Та колись давно я навіть працював на проєкті де був Immutable+Redux, так що це рішення не нове.)

PS: знайшов цікавий проєкт The Benchmarks Game, який порівнює більш ніж 20 мов на декількох простих прикладах. Зокрема, там OCaml та Haskell виходять на такому самому рівні як Swift чи Node.js. А, до речі, Node.js виглядає як найшвидша з популярних “високорівневих” мов.


24.08.2024

Функціональні структури даних

Теоретична частина: поставив собі питання — яким чином функціональні мови здатні… функціонувати, коли вони ніби повинні без кінця копіювати значення для збереження незмінності? Це ж повинно бути надзвичайно неефективно, особливо коли структури даних більшають.

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

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

Виходить, що чисто функціональна мова (або чисто функціональна бібліотека) здатна хоча б наблизитись за швидкістю до імперативної. Що досить суттєвий аргумент за використання функціональної мови “від природи” замість ФП імперативною мовою, що я відразу вирішив перевірити на JavaScript.

Та тут практична частина: зробив невеличкий тестовий скрипт; задача — підрахувати кількістю ключів у SteamDB, довших за 5 символів. Така досить класична функціональна задача на поважному обсязі в 163 Мб даних. Що маємо:

  1. Функціональне рішення лише у 2 рази повільніше за імперативне (яке не приводжу заради стислості):
json.reduce((a, r) => a + Object.keys(r).filter((k) => k.length > 5).length, 0);
  1. Таке саме рішення, але з використанням відомої бібліотеки Immutable, в 10 разів повільніше за попереднє! Висновок: на практиці ціна функціональних структур даних перевищує витрати на копіювання традиційних.

  2. Та саме цікаве: думаю, ну добре, але ось ClojureScript точно повинен робити все правильно та ефективно. Але ні: код на ClojureScript був ще в 4 рази повільніше за Immutable, тобто у 80 разів повільніший за імперативне рішення!

(reduce + 0 (map
  (fn [r]
    (count (filter #(> (count %) 5) (keys r))))
  json))

Висновок: я вірю, що якщо взяти низькорівневу мову та реалізувати функціональні структури близько до заліза, можливо, з власним керуванням памʼяттю, то це може бути швидко (Тобто, наприклад, варіант Haskell, а можливо й Clojure без -script.) Але в імперативній мові вони навряд чи зроблять функціональний код швидше.


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, тож не легше зʼясувати, що залишилося за бортом. Але мало-помалу переїхати вдасться, а далі конфігурація вже буде стабільною.