Вітаємо в нашому другому тематичному місяці, Mechanical March. Цього місяця ми зосереджуємося на системних мовах, тобто тих, які компілюються в машинний код.
Це частково допис, а частково транскрипт відео про Mechanical March. Я коротко розповім про цей місяць, а потім ми трохи розглянемо системні мови, їхню еволюцію в історії, переваги й недоліки компіляції в машинний код, а також швидко поглянемо на кожну з обраних мов. До мене знову приєднується Ерік, який у другій половині розповідатиме більше. Але я почну з практичної інформації.
Тож спершу про обрані мови цього місяця. Це C, C++, D, Go, Nim, Rust, V і Zig. Щоб отримати бейдж Mechanical March, потрібно виконати пʼять вправ однією з цих мов. Наш трек із Go має один із найкращих силабусів на Exercism, тож дуже раджу спробувати. Ми в Exercism також великі прихильники Nim, адже це відносно проста мова для старту й дуже легко нею писати, тож неодмінно раджу спробувати і її.
У нас також є пʼять обраних вправ, які варто спробувати:
- Звʼязаний список або простий звʼязаний список (залежно від мови): виділення й звільнення памʼяті, вказівники
- секретне рукостискання: побітові операції
- панграма: цикли
for, рядки тексту (англ. string) і символи - решето: масиви, цикли
for - двійковий пошук: масиви, цикли
Є новий бейдж, який я анонсував у відео з оновленнями кілька днів тому, і його дають за виконання всіх пʼяти обраних вправ тематичними мовами. Тож, щоб отримати цей бейдж, потрібно протягом року виконати всі ці вправи якоюсь із системних мов.
У нас також готується багато цікавого: інтервʼю з кількома людьми з команди Go Core, сподіваємося, і з Rust та деякими іншими мовами. Протягом місяця також буде багато прямих трансляцій. А ще скоро зʼявиться мерч Mechanical March!
Тож занурмося трохи глибше в технічну частину.
Для чого використовують ці мови?
Ми використовуємо їх у різних частинах Exercism. Наш CLI написаний на Go, наш внутрішній інструмент керування треками під назвою configlet написаний на Nim, а ключова бібліотека, яка рахує рядки коду в рішеннях, написана на Rust. Еріку, чому для цих частин інструментарію ми обрали саме ці мови?
CLI:
- Думаю, ми обрали Go, бо з нею Катріна була найбільш знайома.
- Go чудово підходить для таких невеликих інструментів командного рядка.
- Код на Go досить простий, тож робити внесок легше
- Виконувані файли Go легко розгортати, бо їм не потрібне середовище виконання
- Go добре виконує перехресну компіляцію
Nim
- Nim має більшість тих самих переваг, що й Go
- Не було багато людей, які знали Go і могли б підтримувати CLI, тож ми перейшли на Nim
Де ще можна зустріти ці мови?
- Будь-де, де важлива швидкодія (драйвери, ігри, операційні системи, системи збірки й компілятори)
- Будь-де, де обмежені ресурси (наприклад, вбудоване програмне забезпечення)
- Будь-що, що має бути дуже портативним, тобто працювати на багатьох різних платформах
Що таке машинний код?
Тож, як я вже казав, ці мови в Mechanical March вирізняються тим, що компілюються в машинний код. Пояснімо трохи, що таке машинний код і що таке, на противагу йому, байт-код.
- Машинний код - це код, який може виконуватися на машині безпосередньо.
- Байт-код, на противагу цьому, потребує іншого коду, який його інтерпретує або компілює в машинний код. Тож байт-коду потрібен проміжний крок, перш ніж його можна буде виконати.
Які переваги й недоліки машинного коду порівняно з байт-кодом?
Переваги:
- Швидший запуск (без кроку JIT-компіляції)
- Менший обсяг памʼяті (середовище виконання не завантажується, байт-код не тримається в памʼяті, чудово для вбудованих систем)
- На цільовій машині не потрібно встановлювати середовище виконання (важливо, щоб контейнери Docker залишалися малими)
Недоліки:
- Непортативний. Байт-код портативний, а скомпільований машинний код залежить від платформи
- Неможливі складні оптимізації, як-от профільована оптимізація (визначення найкращого способу компіляції (байт)коду після того, як він якийсь час попрацював)
Зауваження: можливі гібридні підходи, коли мова компілюється в байт-код, а потім інший інструмент компілює цей байт-код у машинний код.
Еволюція системного програмування
Гаразд. Тож трохи розгляньмо еволюцію деяких мов цього місяця. Почнімо з C, з якої все починалося, і з того, як із неї виросла C++. Тож трохи про ці мови.
C - це дуже низькорівнева мова. Вона сприймається так, ніби ми лише трохи вище за машинний код. Це робить її дуже потужною й добре оптимізовною, але водночас дещо схильною до помилок (наприклад, винятки через нульові вказівники та переповнення буфера). Керування памʼяттю тут повністю ручне, а отже, це відповідальність програміста, що може призводити до помилок і витоків памʼяті. C++ схожа на C, але з підтримкою обʼєктно-орієнтованого програмування. Вона все ще досить низькорівнева й вимагає ручного керування памʼяттю. І C, і C++ дозволяють писати вбудований асемблер (ASM)!
А що з новішими системними мовами? Як вони еволюціонували?
Усі сучасні мови системного програмування підтримують автоматичне керування памʼяттю: або через підрахунок посилань, або через збирач сміття, або через якийсь інший механізм.
Ранні мови системного програмування підтримували нульові вказівники (Тоні Гоар назвав це своєю «помилкою на мільярд доларів»). Вони сумно відомі тим, що призводять до помилок під час виконання та вразливостей. Багато сучасних мов позбулися null або принаймні вимагають докласти зусиль, щоб його використати.
Ще одна зміна: раніше типовим було змінне значення, а тепер типовим стає незмінне. Наприклад, у Rust і Vlang значення типово незмінні, і змінність доводиться вмикати явно.
Усі новіші мови підтримують взаємодію з C (або C++), адже дуже багато коду написано цими мовами.
Ще цікаво те, що деякі новіші мови не компілюються в машинний код напряму, а роблять це через інші інструменти. Наприклад, Rust і Zig використовують LLVM, а Nim дозволяє користуватися найрізноманітнішими компіляторами. Це називається транспіляцією.
А що з макросами й метапрограмуванням?
Тут є цікавий розкол щодо макросів і метапрограмування. Макроси в C/C++ потужні, але мають дещо сумнівну репутацію складних у роботі. Rust, Nim і D пропонують потужне метапрограмування, але роблять це набагато приємніше. Натомість VLang і Zig прямо називають відсутність макросів перевагою своєї мови, а в Go підхід інший: go generate.
Системні мови мають репутацію досить низькорівневих. Чи це все ще справедливо?
Усі новіші мови працюють на вищих рівнях абстракції, ніж C/C++. Наприклад, Rust, D і Nim також дозволяють писати код у дуже функціональному стилі. Nim і D навіть мають поняття «чистих» функцій.
Знайомство з мовами місяця
Тож було б добре розглянути кожну з мов по черзі. Усі мови мають спільні риси: усі вони зі строгою статичною типізацією. Але подивімося, чим вони різняться. Почнімо з C?
C
- Розробив Денніс Рітчі
- Одна з найстаріших і, ймовірно, найуживаніша мова у світі
- Безліч програм написано на C, наприклад Unix і Linux
- Дуже впливова (згадаймо, що існують «C-подібні» мови)
- Ручне керування памʼяттю
- Дуже швидка (близько «до заліза»)
- Працює будь-де
- Ідеальна для вбудованих систем
- Досить невелика мова
C++
- Розробив Бʼярне Струструп
- Наступниця C, але з доданою обʼєктною орієнтацією (C з класами)
- Допомогла популяризувати обʼєктно-орієнтоване програмування
- Більше високорівневих можливостей мови, ніж у C
- Підтримує узагальнене програмування через шаблони
- Додає підтримку модулів через простори імен
- Багато ігор (рушіїв) написано на C++, як і значні частини Windows
- Ручне керування памʼяттю
- Досі розвивається, регулярно додаються нові можливості (велика специфікація)
D
- Розробив Волтер Брайт, а згодом долучився Андрей Александреску
- Спочатку задумувалася як перероблена C++ (з урахуванням її «помилок»), але черпає натхнення з багатьох інших мов
- Мультипарадигмова: підтримує імперативне, обʼєктно-орієнтоване й функціональне програмування
- Легка взаємодія з C/C++
- Уніфікований синтаксис виклику функцій
- Обчислення функцій під час компіляції (наприклад, генерація скінченного автомата для регулярного виразу під час компіляції)
- Підтримує функціональне програмування та «чисті» функції
- Багато можливостей для безпеки
- Безпека памʼяті через @safe
- Контракти (передумови й післяумови, інваріанти)
- Чисті функції
- Акцент на модульних тестах, які лежать поруч із кодом, який вони перевіряють (для цього довелося зробити виняток на сайті Exercism :))
- Unicode
Rust
- Розробив Ґрейдон Гоар, співробітник Mozilla Research, згодом Mozilla офіційно його прийняла, а тепер він є частиною Rust Foundation
- Мультипарадигмова: підтримує обʼєктно-орієнтоване (але з власними правилами, наприклад без успадкування), імперативне й функціональне програмування (типи Option/Result, зіставлення зі зразком)
- Багато нових інструментів написано на Rust (наприклад, SWC, а також Gleam; це друга мова, яку офіційно підтримує ядро Linux; ми залежимо від неї в лічильнику рядків коду)
- Акцент на надійності та швидкодії
- Побудована на LLVM
- Найулюбленіша мова в опитуванні StackOverflow протягом останніх 7 років
- Швидка, зокрема завдяки мінімальному ядру та стандартній бібліотеці
- Безпечна: і памʼять, і потоки захищені завдяки володінню та часам життя; типово все незмінне
- Потужна система типів, яка ловить багато помилок під час компіляції (особливо повʼязаних із памʼяттю). Компілятор видає дуже корисні повідомлення про помилки
- Усе необхідне в комплекті: компілятор, інструмент збірки, форматувальник, менеджер пакетів, інтеграції з IDE
- Чудова документація (є навіть величезний документ про те, як працює компілятор)
- Портативна: компілюється в один статичний бінарник, і середовище виконання не потрібно встановлювати
- Легка взаємодія з кодом на C
- Абстракції з нульовою вартістю
- Безстрашна конкурентність
- Макроси
Nim
- Розробив Андреас Румпф (спочатку мова називалася Nimrod)
- Синтаксис натхненний Python
- Мультипарадигмова
- Використовується в Exercism у configlet
- Чудова швидкодія: ітератори без накладних витрат, перевага стековому розміщенню типів-значень
- Сучасна виразна система типів: виведення типів, кортежі, узагальнення, сумарні типи, async/await
- Збирач сміття, але з підтримкою детермінованого керування памʼяттю (кілька варіантів керування памʼяттю)
- Виконання коду під час компіляції
- Уніфікований синтаксис виклику
- Макроси: мову легко розширювати
- Система ефектів: побічні ефекти закодовані в системі типів
Go
- Розробили Роберт Ґріземер, Роб Пайк і Кен Томпсон у Google
- Переважно імперативна й процедурна, підтримує підхід, схожий на обʼєктно-орієнтований (але без успадкування)
- Використовується у великих проєктах, як-от Docker і Kubernetes. Також чудово підходить для бекендів і CLI (наприклад, esbuild)
- Прагне бути настільки простою, щоб її можна було тримати в голові (мало синтаксису)
- Безпека памʼяті завдяки збирачу сміття
- Швидка: швидка компіляція, швидкі тести й швидке виконання. Вбудована підтримка написання бенчмарків!
- Має власні правила: багато зусиль докладено до того, щоб впливати на стиль коду на Go: мало синтаксису, форматування коду через go fmt, інструменти для перевірки коду на ідіоматичність, помилки там, де в інших мовах лише попередження (наприклад, невикористані змінні), документація зі списком ідіом Go, яких варто дотримуватися
- Легка система типів, яка робить Go дуже гнучкою (підтримує виведення типів)
- Портативна: компілюється в один статичний бінарник, і середовище виконання не потрібно встановлювати. Легка перехресна компіляція. Конкурентність через горутини (легковагові) та спілкування через канали
- Структурна типізація через інтерфейси (схоже на качину типізацію, але зі статичною перевіркою)
- Обробка помилок: мова заохочує перевіряти помилки й реагувати на них
VLang
- Розробили Александр Медведніков і Делян Ангелов
- Натхненна Go:
- Та сама стратегія «лише один спосіб робити речі»
- Та сама стратегія «мінімум синтаксису»
- Корутини
- Відмінності від Go:
- Немає nil/null, натомість використовує тип результату
- Типово незмінна
- Сумарні типи (функціональні)
- Інтерполяція рядків тексту
- Менше середовище виконання й менші бінарники
- Зіставлення зі зразком
- Працюють над тим, щоб зробити збирач сміття необовʼязковим (autofree)
- Взаємодія з C без накладних витрат
- Генерація документації з коду
- Швидкий компілятор, який споживає мало памʼяті
Zig
- Розробив Ендрю Келлі
- Досить мало синтаксису (файл PEG-граматики на 500 рядків)
- Виконання коду й рефлексія під час компіляції
- Прагне бути «очевидною»: жодного прихованого потоку керування, прихованих виділень памʼяті чи макросів і метапрограмування
- Ручне виділення памʼяті
- Підтримує різні алокатори
- Функції стандартної бібліотеки, які виділяють памʼять, приймають параметр-алокатор
- Тестовий фреймворк уміє виявляти витоки памʼяті
- Безпека:
- Помилки - це значення, і їх потрібно обробляти
- Немає null, є опціональний тип
- Тести можна писати в тому самому файлі, що й код (як у D)
- Використовує LLVM як бекенд
- Уміє компілювати код на C/C++
- Легка взаємодія з C
- Легка перехресна компіляція
Висновок
На цьому й зупинімося, бо, уявляю, у всіх на цьому етапі вже повна голова.
Сподіваюся, це був корисний і цікавий вступ до обраних мов цього місяця. Сподіваюся, досліджувати ці мови буде дуже цікаво. Ми з Еріком обидва радо почуємо, які мови обрані й як вони зайшли, тож, будь ласка, напишіть у коментарях або на форумі!
Дякуємо за перегляд!