Не осилил Rust и написал свой язык программирования
«Конечно! Вот текст для поста на Пикабу:»
Привет, Пикабу! Данный пост изначально задумывался для Хабра, но я подумал что там мне напихают не глядя и не в панамку, поэтому начну отсюда :)
Данный пост с высокой вероятностью является унылым калом, к тому же душным.
Продолжайте чтение на свой страх ирисок.
> Преамбула
Прежде чем перейти к сути, накину контекста: я не программист и каких-либо полноценных проектов я никогда не писал. Иронично, но я преподаю программирование для детей и подростков лет 12-14. По серьезному: не "давайте на Scratch сделаем так, чтобы котик двигался влево-вправо", а на Python задачки на алгоритмы, библиотеки, пишем игры на фреймворке Pyxel (ранее сидели на Arcade). PyGame дичь, не используем. В общем чужими руками я написал гораздо больше, чем своими. А еще я чуть чуть дизайнер и тридешник. Несмотря на это всё я 17 лет так или иначе "трогал" кучу языков: C, C++, C#, JS, Lua, PHP, HTML+CSS, даже ASM, так что знаю "как бывает".
Это как жить в семье музыкантов - сам, может, на скрипке играть и не умеешь, но ноты знаешь. Обвинения в профанации, мол "как ты можешь преподавать без знаний" - не принимаются. У нас кружок робототехники, и мы в первую очередь работаем с микроконтроллерами, так что если вы сами не знаете что такое АЦП, как он работает на "железном" уровне, что такое прерывания и конечный автомат, кружок бисероплетения на три блока ниже. Так что база у нас на языке C, а Python для кругозора.
> Теперь к сути:
T: -45d
Сижу, мучаю нейронку на тему концепций, которые так и не смог понять: Лямбды, Декораторы, вот такое. Вроде как суть ясна, но не понятна проблема, решением которой выступают эти инструменты.
Т: -41d
Пришёл к выводу что мне не близки концепции с высокой долей "магии".
T: -33d
Нейронка настоятельно советует попробовать плеяду "современных языков системного программирования": Rust, Go, ZIg, Odin.
T: -31d
Установлен Rust, изучаем базу. Cargo очень нравится, прям кайф. Первую программу свайбкодил, читаю. Очень больно. А это что за бред? М, понятно, но зачем делать это так... Круто что всё так быстро собирается, плохо, что код плохо читается. Обработка возвращаемых ошибок и значений это абзац:
Rust:
match read_file_content("hello.txt") {
Ok(text) => println!("Содержимое: {}", text),
Err(e) => eprintln!("Ошибка: {}", e), }
T: -30d
Пробую писать сам, спотыкаюсь на каждом символе, мало того что куча всяких <> :: => !, семантика страдает, короче пахнет C++, который я сильно недолюбливаю (C люблю). Ну и да, корни у Rust очень сильно растут в C++. Спасибо, я пошёл.
T: -27d
Пробую Zig, Odin, но и там видны рога от C++, который "испортил" С, который родом их 70-х. Да, прикольные модели управления памятью, но чет фу (ну зачем := делать???). Хочется чтобы читалось ненапряжно как Python, но компилировалось, было явно и без сборщика мусора.
Т: -25d
Понимаю, что толком никто не занимался нормально UX языков программирования. Да, вроде как существуют всякие Ruby, но как будто когда заканчивались базовые средства "разметки" кода, в ход шло всё подряд:
:: < > & * # @ !
И я не про оператор НЕ (!) а про макрос из примера выше.
То есть какой то умный дядька (явно умнее меня!), решил, что брать аккорды на клавиатуре по 3 раза за строку это нормально. Ну логика ясна: символов меньше, значит и писать тоже меньше, всё же верно? Верно ведь?
Как по мне, этот минимализм абсолютно заслуженно боготворят люди, которые пишут тысячи строк кода в месяц. Только вот я видимо альтернативно одарен и считаю что нужно что-то менять.
Я создам свой язык программирования. Без блэкджека.
T: -24d
Ну начнем с базы, что мы выкинем в первую очередь?
; в конце каждой строки. Этот символ не играет роли, всё равно писать по 3 выражения в строку это моветон.
А вот { } для блоков кода оставим, это база. Уровни вложенности из Python засчет отступов иногда сложно считываются.
Ну и ( ) вокруг условий тоже пожалуй не особо нужны, если кто захочет - пожалуйста, в рамках логического/математического выражения они всё равно валидны.
А вот дальше интереснее.
Если if / else if / else мы не трогаем, всё логично, то вот первым на очереди на рестайлинг у нас идет switch / case . Вот к нему у меня много вопросов. Во первых в C его синтаксис чужероден (или я один это замечаю?):
С Language:
switch (x) {case 1:
printf("Один");
Простите, с каких пор в C у нас блоки кода идут просто после двоеточия?
Но самая главная проблема - семантике.
Семантика — это смысл, значение языковых единиц (слов, фраз, конструкций), в отличие от их звучания (фонетики), формы (морфологии) или порядка слов (синтаксиса).
Switch означает "Переключатель". Других значений нет.
Что мы переключаем? Да, мы переключаем поток выполнения, "переводим стрелку". Но это "вид со стороны ассемблера", а не человека.
Case означает "случай" / "футляр". Очевидно, речь подразумевается "в случае".
Однако вместе оно как-то... Ну, не бьется, что ли. Понятно, что все давно привыкли. Но изначально это была надстройка над goto уши от чего торчат до сих пор (кто нибудь когда нибудь видел goto вживую? по мне это как легенда из старых ужастиков). Таким образом по факту case заменяет метку для прыжка. В Rust заменили на match ("сопоставить"). Ну, допустим.
Итак, как нам это заменить? Начнём с наших намерений: мы хотим написать нечто вроде многоступенчатого else if , только лаконично. Как это произнести алгоритмически?
Когда X
равен Y, тогда ...
равен Z, тогда ...
"равен" можно выкинуть, нет смысла писать сравнение на каждой строке. Но стоит иметь некий визуальный маркер. Поэтому использую is ("является"):
when x {
is 1 { ... }
is 2,3 { ... }
else { ... }
}
Мне - нравится. Как продолжение фраз "Пока" и "Если".
Кого дальше потрогаем?
while - оставляем как есть, претензий нет. А вот loop как оператор бесконечного цикла прямая замена while True - тоже забираем из Rust/Zig, это хорошая идея.
А вот for придется "переодеть". Как ни крути, все варианты цикла for , кроме foreach обладают сомнительной семантичностью. Как в Python?
for x in array:
Как это прочитать? Для каждого элемента ИКС в МАССИВЕ (делаем: ...). Короче вроде и норм, но мой вариант:
iterate array as x {...}
Итерировать / Перебирать МАССИВ как ИКС.
По сути, мы просто поменяли два аргумента местами и заменили слово из трех букв на слово аж из семи. Но как по мне, это пошло на пользу читаемости. Не пытаемся сократить до iter, пишем полностью.
*вздох*
enum .
Что мы тут нумеруем? Буковки? Мы именно пронумеровать хотим? Или нам всё таки нужны читаемые оболочки над числами для использования в качестве читаемых статусов/состояний?
label - лучше отражает суть происходящего, ИМХО.
Теперь к вещам посущественнее: синтаксис объявления функции.
Rust
fn sum_positive(xs: &[i32]) -> i32 {...}
Вы тоже это видите? к fn нет вопросов - кратко, лаконично. Лучше, чем в С. А вот что дальше пошло не так?
: &[ ] ->
Это всё буквально мусор. Тут только в C++ сделали, как мне кажется, хуже.
-> должен показывать что "возвращаем такой то:", но это буквально нужно рисовать стрелочку.
Про амперсанд и прочее в аргументе пока молчу.
Zig показывает как надо:
Zig
fn sumPositive(xs: []const i32) i32 {...}
Ну ладно, давайте представим что хотим вернуть структуру?
Тогда пишем явно, гордо, твердо и четко: returns . А чтобы не разводить внутри ( ) свалку, воспользуемся технологией древних: просто сделаем как в C: тип, затем имя. Вместо & , если хотим передать значение по ссылке, пишем прямо: direct .
Получаем:
fn sum_positive(i32 List xs) i32 {...}
(в лучшем случае (не пишем returns для простых типов))
fn find_min_max(direct List i32 myarr, bool x) returns struct {i32 min, i32 max} {...}
(в худшем случае (ну почти))
Ой, плаки-плаки, всё слиплось, непонятно. Конечно, мы же на пикабу, а не в IDE, где всё подсвечено. Зато читаемо и без мусора.
Не поймите неправильно, все эти символы подразумевались как однозначное определение происходящего, визуальный маркер. Проблема возникает тогда, когда эти маркеры идут сплошной стеной - они утрачивают свое значение, свой вес. И превращаются в мусор.
Те, кто изо дня в день пишет этого не замечают, для них это само собою разумеющееся. И это понятно. Но давайте помечтаем.
Давайте к чему то попроще.
i++ / i-- - вообще то это хорошая вещь. Если её запретить использовать внутри других выражений, только как отдельное выражение. Краткая запись i += 1
Как создавать переменные? Когда я только-только изучал C где то в 13 лет, я думал что int в начале строк это что то типа инициализировать (init). Но нет, с этого (указания типа), у нас начинается объявление. В Python не парились: присваивание новому имени? Значит переменная. В других языках где-то let , где то var .
var не имеет семантики. Это огрызок от variable.
let меня вообще меня фрустрирует. Оно с LISP, с 1958 года родом.
new i32 x = 0
Новое? Новое. Вопросы? Конструктор объектов не обеднеет.
Кстати о них. Структуры с методами - мой выбор. Никакого наследования и т.д.
Обращение к полю через my . Хотим спрятать поле - hide . Хотим спрятать функцию - local .
Пространства имен: никогда небыло проблем просто с обращением через точку типа
module.object.method()
Давайте опустим пока управление памятью и многопоточность, потому как там можно совсем завязнуть, но yield , async и await тоже заслуживают камня в огород.
Давайте еще чуток пофантазируем. Допустим, функция потенциально возвращает ошибку. Почему бы не маркировать её сразу как danger ? И другие функции тоже имеют право её вызывать только имея эту метку.
danger fn read_file(Path file) returns Text {...}
Ок, а обработка ошибок?
txt = read_file(file) on error {output("не смогли прочитать файл :( ")}
Ничего страшного, парсер не подавится - on error это единый токен. Можно докрутить в плане явности типа ошибки, но пока так. В ту же калитку тогда почему бы не:
on event // для игровых циклов или embedded, часть многопоточности
on interrupt // сладость для embedded
Кстати, вы задумывались над print( ) ? Это же буквально "печатать". На бумаге. Это легаси со времен, когда терминал был печатной машинкой в натуре.
//Высокоуровневое (ввод/вывод из консоли):
input( )
output( )//Низкоуровневое (запись, чтение файла/потока):
read( )
write( )
Кратко про типы:
// Базовые (оч понравилось у Rust)
i8, i16, i32, i64
u8, u16, u32, u64
f32, f64
bool
char
// Алиасы
Int // Алиас для i64 (зависит от архитектуры)
Float // Алиас для f64 (зависит от архитектуры)
Byte // Алиас для u8
Word, DWord, QWord // На всякий случай
// Составные и расширенные
[T; N] // Статический массив
List T // Динамический массив
Vec2, Vec3, Vec4 // Нужны примерно везде
String // "Эффективная" строка (просто массив символов)
Text // "Мощная" строка (с методами и т.д.)
Path // Специальный тип для путей
Angle // Специальный тип для углов
Range // Диапазон чисел
struct // Структура
label // Вместо enum
+ литералы для размеров данных и времени: 3kb, 500ms
Да, прямо в ядре. В бинарник это тащить не нужно, если не используется, не вижу проблемы.
Вообще, почему math вместе со всей тригонометрией и даже корнями отдельно? Боитесь засорить namespace? Кстати не вижу никаких проблем в оператора в духе and , or , а значит и для xor , тогда ^ можно законно вернуть возведению в степень. А можно пойти дальше и использовать idiv и mod (вместо %) как inline операторы.
Так и что дальше?
T: -12d
Берём в охапку Codex и вайбкодим транспилятор всего этого добра на C. И сверху менеджер проектов в духе Cargo. Как назовём? Пускай пока будет Skadi.
https://github.com/eoshipnyagov/Skadi-Language/tree/master
T: -3d
Глядя на получившийся проект, пусть в стадии идейного демонстратора, я бы сказал что у меня получился некий учебный язык для gamedev, cli и embedded (удивительно (нет)). Нет, он не станет популярным. Не убьет Rust. Но, возможно, станет маленькой крупинкой взгляда со стороны на устоявшиеся стандарты индустрии.
T: -1d
Зачем это делать, если слон в комнате в виде вайбкодинга дышет в ухо? Ну. Тогда тем более читаемость выходит на первый план "простоте записи". Нейронки хвалят получившийся синтаксис, хотя справедливо указывают на некоторые конфликты с устоями и проблемы однозначности.
Важно, что я не пытался сделать код "псевдоанглийским", я просто немного причесал UX на свой лад.
T: -3m
«Если нужна более строгая формулировка — дайте знать, я перепишу в академическом стиле»

























