Помнится, давненько я писал статью про свой язык программирования. Мало кто её одобрил, да и я по мере накопления опыта продолжал метаморфоз своей затеи. В этой статьи я отвечу на заданные вопросы, расскажу о том, как идея родилась заново, какие у меня планы, проблемы и так далее по списку.
Описание проекта после перерождения
За всё своё время в IT я получил массу опыта в самых разных языках программирования. Каждый хорош по своему, и у каждого есть свои недостатки. А ещё есть нишевые языки - они заточены под конкретное применение, поэтому, естественно, подойдут не всем. Итак, прежде, чем создавать язык, я решил четко определить его свойства и подвести все плюсы и минусы концепта.
Язык однозначно будет компилируемым. Скриптовые языки хороши, но для моих целей они не особо подходят. Так же, это сделает Honey альтернативой другим компилируемым языкам.
Я решил отказаться от мета-программирования. Оно зачастую не улучшает, а усложняет код, делая сложнее его расширение и поддержку в будущем.
Никаких макросов и текстовой обработки. Уже давно разжевывали, почему это плохо.
Нет шаблонам! Хотя это частично пункт 2, но я решил его обособить, ведь шаблоны - это в первую очередь про zero-cost абстракции и универсальность кода. Но практика на С++ показала, что, не смотря на благие намерения такой фишки, она может нести за собой катастрофические последствия, начиная с повышенного порога входа и заканчивая boilerplate-кодом и прочими проблемами пункта 2.
Отказ от полноценного ООП. Полиморфизм, наследование и прочие возможности ООП неоспоримо магия для программиста, что позволяет писать необычайно удобный в поддержке код. Но обилие функционала порождает собой проблемы пунктов 2 и 4. Истина познаётся в балансе :)
Поощрение структурного программирования семантикой языка. Вот что действительно делает код чистым, понятным и поддерживаемым, так это структурное программирование. Оно позволяет Создавать структурированный логически код, а следуя нескольким простым распространённым правилам в СП код становится действительно лаконичным.
Строгая типизация. Конверсии типов возможны только между целочисленными типами и из целочисленного в тип с плавающей запятой. иные преобразования нужно реализовывать явно в целях избежания неожиданных проблем
Безопасное программирование. Управление памятью автоматическое, а любое возникновение ошибки сопровождается выбрасыванием исключения.
Отсутствие рефлексии. Она усложняет как программу, так и её код в целом.
Мощные вычисления во время компиляции. Всё, что можно посчитать или выполнить во время компиляции, будет посчитано или выполнено. Подробнее смотрите далее.
максимальная простота синтаксиса. синтаксис должен быть прост и лаконичен. Так же, под простотой подразумевается в том числе сложность парсера, который должен код обрабатывать
Совместимость с уже существующими библиотеками. Встроенная поддержка библиотек на С.
Независимость кода от среды. Где бы код на Honey не запустился, результат всегда будет ожидаемый.
Стандартная библиотека. по умолчанию будет модуль libc, который полностью включает в себя биндинги к стандартной библиотеке С. Так же будет своя стандартная библиотека, опционально. она будет в модуле std.
Подробнее про сам язык
Синтаксис. Он будет подобен Rust ибо он весьма приятен глазу и интуитивен. То же касаемо именования встроенных типов. Вот пример функции main на Honey с использованием libc:
import libc;
fn main(args: str[]) -> i32 {
puts("Hello, World!");
}
Определение типа возвращаемого значения опционально. Если оно отсутствует, по умолчанию применяется тип void. После всего, может идти несколько слов, обозначающих свойства функции. например:
fn pow2(n: i32) -> i32 nothrow noct { return n**2; }
Флаг nothrow логично названию обозначает, что внутри функции никаким образом не может произойти исключение. чаще это требуется только для упрощения процесса компиляции. Не рекомендовано к использованию, но вам никто не запретит. noct запрещает выполнение функции во время компиляции. Это бывает необходимо для ёмких функций, которые предпочтительнее выполнять во время работы программы.
Структуры
Структуры определяются весьма знакомым всем образом:
struct vec2 {
float x; float y;
}
Заметьте: бесящая точка с запятой после этого не обязательна. Структура всегда является типом, то есть не нужно писать что-то из С на подобии typedef struct vec2_s vec2_t;
. Но погодите: а как же ООП? Я же сказал выше, что я не отказываюсь от него полностью, а лишь намеренно ограничиваю его возможности в семантике самого языка. Это правда. у структур могут быть методы. Методы бывают трёх типов: операторы, кон/деструкторы и обычные методы. Например, выше определенная структура может быть инициализированная как vec2{}
, vec2 0
, vec2 {x_val, y_val}
. В чём разница:
Первый конструктор - это конструктор по умолчанию. он устанавливает все поля структуры в их изначальные значения. Второй - нуль-конструктор, он приравнивает все поля к нулю либо их нулевому состоянию. Третий конструктор - обычный, просто устанавливает полям их значения. Так же можно определить пользовательский конструктор, создав метод с названием ctor
. Помимо конструкторов в языке так же есть инициализаторы. Они отличаются от конструкторов тем, что вызываются после выполнения конструктора по умолчанию, и может иметь самые разные назначения. для использования инициализатора нужно использовать синтаксис new vec2(args)
. Для определения инициализатора нужно создать метод с названием operator init
. Так же в языке есть перегрузки операторов, которые ничем принципиально не отличаются от перегрузок из С++, кроме индексации структуры - для этого нужно два оператора operator setitem(isize index, T value)
и operator getitem(isize index) -> T
. Итоговый код структуры vec2 на языке Honey будет выглядеть так:
struct vec2 {
float x; float y;
// пользовательские конструкторы или инициализаторы
// для этой структуры не требуются
fn operator+(vec2 other) -> vec2 {
return vec2{this.x + other.x, this.y + other.y};
}
// и все остальные операторы подобным образом
fn operator getitem(isize i) -> float {
switch i {
case 0 { return this.x; }
case 1 { return this.y; }
default { throw OutOfBouds; }
}
}
}
Как мы можем заметить, простейшее ООП есть. Помимо этого присутствует наследование с единственным родителем.
Импорт файлов
В Honey нет заголовков. Компилятор многопроходный, так что нет необходимости создания прототипа для реализуемой в пределах кода Honey функции. Импорт работает, по сути, так же, как и inline
из препроцессора С, но вместо текстовой подстановки модифицируется АСТ кода.
Циклы
Язык поддерживает следующие типы циклов:
С-подобный: for (init; cond; step) body
Итеративный: for iterator : iterable body
Вечный: for ever body // сделано намерено для дополнительных оптимизаций
С предусловием: while cond body
С постусловием: do body while cond
В целом тут всё и так понятно, только вот перед телом цикла можно указать название цикла (суть циклы являются именованными), поэтому конструкции break и continue могут содержать следующим словом название цикла, к которому оно относится.
Пример со сферической графической библиотекой в вакууме:
for ever mainloop {
for event : getEvents() eventloop {
if (event.type == QUIT) break mainloop;
}
clearWindow(); drawBox(...); flipBuffer();
CapFPS(60);
}
Здесь наглядно видно, какое упрощение кода происходит. Кто-то скажет: "Это можно легко сделать метками и goto". Не стану обосновывать, почему это плохая практика.
Выполнение во время компиляции
Наконец, самое вкусное. Всё, что может быть посчитано или выполнено во время компиляции, будет посчитано или выполнено во время компиляции (прощу прощения за тавтологию). Если нужно пометить функцию/блок кода/цикл/переменную как вычисляемую только во время исполнения, достаточно добавить флаг noct. Так же, для увеличения возможностей запекания значений будет создан встроенный в компилятор модуль ct, который будет включать в себя почти все возможные функции, которые будут выполнены во время компиляции. Например, у вас есть файл конфигурации, в котором есть лишь одна строка - номер версии. Не предполагается, что этот файл может изменяться во время исполнения программы, поэтому можно сделать так:
import ct;
version: str = ct.loadFile("version.info");
Данный пример взят с потолка исключительно для демонстрации. На данный момент планируются следующие функции модуля ct:
readFile
writeFile
readLn
writeLn
osExec
jsonLoad
jsonSave
Так же, поскольку в Honey любая переменная может оказаться посчитанной ещё при компиляции, я решил расширить функционал ветвления в языке, и теперь подобное разрешено:
debug: bool = true;
if (debug) {
fn log(msg: str) -> void { ... }
} else {
fn log(msg: str) -> void { }
}
И так можно с любым выражением любой сложности. То есть, помимо того, что сам язык (естественно) полный по Тьюрингу, таким же свойством обладает компилятор языка. Это даёт разработчику бесконечные возможности в оптимизации и структуризации кода, а так же кастомизации конвейера сборки программы.
Преимущества и недостатки решения
В процессе работы, я решил исследовать потребности программистов в повседневной работе, а так же в развлечениях и нестандартном применении тех или иных инструментов. Помимо этого, я сделал предсказания (возможно, ошибочные), среди кого данный язык может взыскать популярность. Напоследок - список плюсов и минусов решения, старался оценивать максимально объективно - самокритика это тоже полезно для собственного роста.
Потребности программистов в повседневной работе и развлечениях
В первую очередь для бизнеса, программист - это производитель интеллектуального ресурса. Для выполнения своего функционала, он потребляет такой ресурс, как время, то есть преобразовывает время в интеллектуальный ресурс. Время важно в бизнесе по двум причинам: первая - самая очевидная - оно стоит денег. второе - оно важно в бизнес-стратегии. Итак, как же повысить КПД программиста? Упростить его работу, конечно же. Данный язык может взыскать популярность в проектах, связанных с низкоуровневым программированием, либо программированием высокоэффективного прикладного программного обеспечения ввиду своей специфики. Он предоставляет достаточно богатый уровень абстракций по сравнению с традиционным С, и при этом не перегружен, как С++ или Rust. Одним словом - золотая середина. Итак, основываясь на данные TIOBE Index, можно сделать вывод, что на данный момент процент использования низкоуровневых языков программирования составляет около 10,23%. Это физический максимум, который может охватить Honey как язык программирования низкого уровня. Но он, хоть и поддерживает низкоуровневые операции, исключительно низкоуровневым не является. Давайте посчитаем процент использования компилируемых в нативный код языков программирования в целом, результат, основываясь на всё тот же TIOBE Index будет 31.64%. Вот он - физический потолок охвата Honey в сказочно благоприятных условиях. Но зная, как тяжело убедить человека перейти с одного инструмента на другой, если ему уже "так удобно" или "так привычно" или "у меня уже десятки проектов на этом языке", то можно без зазрения совести рубить чисто на десять. Итого, предполагаемый результат охвата Honey, при условии, что о нём будут знать все программисты, использующие компилируемые языки программирования, равен примерно 3.16%. А теперь давайте поделим для честности это число ещё на два, ведь далеко не все программисты сразу узнают о его существовании, результат, ожидаемо, равен 1.58%. Каким бы малым числом результат не казался, это достаточно серьезный результат, ведь он на уровне Delphi/Rust. А Rust, не смотря на скромные 1.16%, на самом деле довольно популярный язык программирования с большой пользовательской базой.
Плюсы решения
Язык имеет весьма низкий порог входа.
Язык предоставляет массу zero-cost абстракций и тонну оптимизаций, не смотря на свою простоту.
Язык переносим и просто в понимании, код на нем редко будет болеть boilerplate'ом или проблемами с поддержкой и/или развитием.
Минусы решения
Непривычный подход к реализации, который может отпугнуть потенциальных пользователей.
Примитивность языка может сыграть в том числе против нас - многие разработчики и компании не будут рассматривать его как достаточно серьезный язык для своего детища.
Спорные решения в реализации, например, отказ от полноценного ООП и рефлексии, может вызвать за собой лавину негатива.
Заключение
В этой статье я гораздо меньше касался реализации языка и затронул по большей части его преимущества, недостатки, философию и маркетинговый потенциал. В следующих статьях будет рассматриваться реализация и вестись лог разработки компилятора и развития языка в целом. Надеюсь, данное чтиво придётся кому-либо по нраву. Эта статья - мой первый исследовательский текст, который является рассказом или объяснением в наименьшей мере. Так скажем, эксперимент. Очень надеюсь, что каждый читатель выразит своё мнение в комментариях. Всех люблю!
Комментарии (22)
ednersky
08.01.2025 21:50Преимущества и недостатки решения
не очень понял цели языков
вот взять golang: цель (явно видная) - поддержка кооперативной и обычной многозадачности на уровне языка и стандартных библиотек
rust: цель (явно видная) - заставить программиста (не путать с компилятором) много думать о вопросах как правильно и где выделять память.
rust+golang: цель (явно видная) - поставить на программистах эксперимент "что будет если их заставить жить в мире, в котором нет исключений?"
и так далее
вот rust не ставил целью на уровне языка поддержать коопмногозадачность и она у него такая же кривая, как у С получилась. А меж тем вопрос асинхронности он центральный в наше микросервисное время
так вот, к чему это я?
а какая цель у Honey? ну, кроме, синтаксического сахара (который местами сомнителен)
TalismanChet Автор
08.01.2025 21:50Косвенно, но я упоминал это в статье. Цель Honey - позволить программисту писать высококачественный код, меньше задумываясь об этом - за него подумает компилятор, ведь правила языка сами по себе способствуют хорошему коду. Хотя я не отрицаю тот факт, что насвинячить можно где угодно и как угодно.
ednersky
08.01.2025 21:50высококачественный код он может писать на любом языке (а может не писать)
чтобы вынудить его писать высококачественный код, Rust заставляет выполнять определенные действия (идёт по дороге «принуждения к качеству» ), а golang, например, дает базовый паттерн прямо на уровне языка (дорога «решить определенную проблему на уровне языка»)
и так далее
а что Honey? TMTOWTDI?
TalismanChet Автор
08.01.2025 21:50Как раз противоположность TMTOWTDI. Конечно, при некоторых условиях Тим Тоуди проявляется, но всё же. Насвинячить можно где угодно и как угодно.
ednersky
08.01.2025 21:50откуда же взялся мем, что тимтоуди является синонимом «насвинячить»?
в моей голове это подпринцип (или один из способов достижения) принципа KISS
TalismanChet Автор
08.01.2025 21:50Не синоним. На моей практике, которая, возможно, не так разнообразна, как ваша, Тим Тоуди приводит к дестандартизации и захламлению кода.
duselguy
08.01.2025 21:50А что с указателями?
Кроме исследования проблем программистов, Вы рассматривали похожие языки Odin, Hare, etc. ?
TalismanChet Автор
08.01.2025 21:50Указателей в языке нет. вообще. для библиотек С используются isize либо cffi.voidp типы.
Пытался искать подобные языки, но что-то не ищется. Каждая моя статья заканчивается тем, что я узнаю о существовании как минимум двух языков программирования, забавно.
ALexKud
08.01.2025 21:50У меня в моей системе требовалась разработка программ для системы тестирования интеллектуальных приборов. Пришлось разработать свой простой язык для интерпретатора, работающего на компьютере тестовой станции. Так как станций десяток, то программа хранится и корректируется в таблице sql. Линейный алгоритм с поддержкой циклов, косвенной адресацией в циклах и т п. Все крутится в sql.. Это конечно нишевые решения, но мой первый опыт в создании скиптового языка с обработкой на sql.
orefkov
08.01.2025 21:50При стремлении к лаконичному синтаксису и бесящих запяточках странно выглядит двоеточие между именем и типом.
А так - ничего революционного не увидел, очередной недо-с++. Про управление памятью вообще ничего не написано, кроме того, что оно автоматическое. Как реализуется? Сборщик мусора, перемещения/заимствования, счётчики ссылок, возможен ли прямой доступ к памяти?
Мне язык точно не подойдёт - нет шаблонов и метапрограммирования - не люблю повторятся.TalismanChet Автор
08.01.2025 21:50По-моему, вы пропустили тот факт, что статья только частично затрагивает фронт-енд языка, а в основном посвящена исследованию того, насколько решение хорошее, и приблизительные догадки о возможной популярности языка.
ednersky
вот никогда не понимал: "кого же она бесила?"
и из-за того, что например разработчиков golang она тоже бесила, у них получился крайне неудобный парсер языка.
получается у тебя, например, длинная строка: у функции много аргументов, или ссылка на структуру (методом которой будет являться функция) много места заняла... а ты не можешь взять и в любом месте вставить перенос строки: из-за отказа от точек с запятой они не всегда могут распознать "это перенос на другую строку или ошибка синтаксиса?" и выдают ошибку синтаксиса, там где другие языки справляются, например
MzMz
и Python тоже периодически бесит, без слэша никак:
SoraVWV
Это скорее вопрос синтаксического анализатора, чем самого факта отказа от точек с запятой.
У меня в языке, например, точка с запятой не является обязательным элементом (фактически она почти всегда игнорируется), а перенос строки влияет только на принадлежность декремента или инкремента, а также на `return`/`throw`/`break`/`continue`
Например, в следующем коде инкремент будет принадлежать к переменной `x` как постфиксный:
Но если перенести инкремент на следующую строку, то он уже будет префиксным и относится к объекту, который будет возвращать функция `f()`:
Отвечая на вопрос "кого же бесила точка с запятой?" — никого. Данный элемент в синтаксисе на данный момент является больше "наследием" и излишком прошлого, чем действительно необходимым терминалом.
Но при этом точка с запятой, как мне кажется, обязана присутствовать как необязательный элемент. В качестве примера можно привести случай, когда метод изменяет объект так, что при неизвестном заранее количестве итераций он вернет одно необходимое значение, из-за чего в циклах не будет необходимости прописывать тело:
Не знаю, можно ли учитывать все это в генерируемых парсерах, так как не использую их. У меня в профиле присутствует пара статей на тему написания синтаксического анализатора (абстрактная теория и отдельно практика), если интересно
Refridgerator
Спасибо, хорошие статьи. Ну а меня раздражает злоупотребление скобочками и ключевыми словами на каждый чих, var и func особенно. В порядке эксперимента даже написал парсер, где мат. выражения типа
cos n arccos x
вместоcos(n*arccos(x))
распознаются вполне корректно. Для этого пришлось добавить операторы "неявное умножение" с приоритетом выше обычного умножения, умножение с приоритетом ниже сложения/вычитания, и с делением аналогично. Прочих идей тоже хватает, но общей картины миллиард первого самого лучшего ЯП пока ещё не сложилось, и самого главного - нет ещё крутого названия для него)kinall
Очевидный вопрос - а почему не
cos(n)*arccos(x)
?Refridgerator
Вот почему, если вопрос про смысл формулы. А если про парсинг - то из-за расстановки приоритетов, математики часто опускают скобки при записи подобного рода выражений. Для вашего случая надо так и писать -
cos n * arccos x
.ednersky
с ++ двусмысленностей немного
однако можно написать
a--; b--; c--;
а без обязательности точек с запятой будет двусмысленность вроде
a - -b - -c--
и таких двусмысленностей много, и именно поэтому (я так считаю) у golang нельзя поставить где попало перенос строки
TalismanChet Автор
Попрошу заметить, что я не отказался от запяточек в целом, я не использую их конкретно после закрывающей фигурной скобки, а так они в языке вполне себе есть, что можно понять по разным примерам кода из статьи.
ednersky
еще раз: golang тоже от них не отказывался
проблемы начались от внедрения их НЕОБЯЗАТЕЛЬНОСТИ
TalismanChet Автор
У меня они обязательны, но удалены там, где они не нужны. то есть, условно, после определения структуры они парсеру вообще не сдались, там нет неодноначности, а вот после операции в коде функции они вполне себе нужны и обязательны.
TalismanChet Автор
Вы, возможно, неправильно меня поняли. Так то запяточки в языке есть, но моё упоминание бесящих подразумевало точки с запятыми после классов или структур в С/++.для определения функции в конце ";" не требуется, а для структуры зачем-то надо. это меня и бесило.