История о том, как я разработал язык программирования
Привет Хабр! Меня зовут Ильдар. Мне 29 лет. Программирую с 2003 года. За свою жизнь создал 4 фреймворка и язык программирования. В этом посте я поделюсь своим опытом, инсайтами, которые я получил при разработке языка программирования BAYRELL Language. Заранее прощу прощения за возможные синтаксические и пунктуационные ошибки в тексте и отсутствие картинок.
История создания
BAYRELL Language я начал создавать летом 2016 года. Тогда я узнал про ангуляр и реакт. Они произвели на меня впечатления прорывных технологий. Я тогда разрабатывал сайты на PHP и использовал Twig в качестве шаблонизатора. И хотел попробовать Angular. Начал искать, как можно прикрутить Angular к Twig.
В целом Twig+Angular работает, но был эффект моргания. Страница рендерилась через Twig на сервере, передавалась клиенту, и там уже рендерился Angular. И страница, пока рендерится у клиента, она моргала. И я тогда задумался, а есть ли клиент-серверный шаблонизатор, но такой, чтобы работал на PHP и JavaScript?
Я поискал в интернете, все рекомендовали использовать NodeJS в качестве сервера, либо V8 движок в PHP, либо микро сервисную архитектуру: backend на PHP, frontend на NodeJS. Только вот незадача. Я делал сайты на WordPress, Yii2. А на хостинге, не было поддержки ни NodeJS, ни V8. Тогда и пришла идея, написать кросс язычный шаблонизатор.
Я начал его делать. Написал за месяц на PHP, потом за другой месяц написал на JS. И быстро понял, что поддерживать два языка, одновременно сложно. Если появлялся, баг, то его нужно было пофиксить и там и там. Очень быстро версии шаблонизаторов стали разными, и расходились по функционалу. У меня было желание добавить Python. Я понял, что разрабатывать отдельно под каждый язык шаблонизатор, очень сложно. Тогда появилась другая идея. Найти транслятор языков, и сконвертировать код шаблонизатора на PHP в JS и в Python.
Я поискал в интернете про PHP to JS. Нашел несколько решений и опробовал их. Результат оказался дивный. Данные решение не полностью покрывали функционал PHP и JS. И как быстро выяснилось, массивы в PHP и JS отличаются. В JS это массивы, а в PHP это еще могут быть объекты. В JS console.log, а в PHP var_dump. Runtime среда, работа с массивами в языках отличаются друг от друга. И с этой проблемой, те решения, которые я рассматривал, не смогли ничего сделать.
Меня посетила следующая идея. Написать язык программирования, который будет транслировать код в несколько языков. Все равно большинство и так пользуются TypeScript, CoffeeScript, Dart, Babel, которые транслирует код в JavaScript. Почему бы не создать язык, который также бы транслировался, но не только в JS, но еще и в PHP, Python и т.п.
Так родилась идея создания BAYRELL Language.
На разработку у меня потребовалось много времени, я несколько раз переписывал язык с нуля. Рабочую версию 0.3 я получил в середине 2018 года. В ней был шаблонизатор и рабочий транслятор. Но я не стал ее выкладывать, потому что она была сырая, хоть и рабочая.
За следующие полтора года, я переписал язык на функциональный стиль и добавил функциональное программирование. Оптимизировал транслятор, и откорректировал JS рендер. И теперь получилась рабочая альфа версия 0.8.3.
Возможности языка
- Код на языке BAYRELL Language транслируется в PHP, NodeJS, JavaScript;
- Встроенный в язык HTML шаблонизатор;
- Рендер в браузере и на сервере;
- Возможность разработки бэкенд api;
- Функциональное программирование;
- ООП;
- Неизменяемые структуры данных;
- Функциональные цепочки вызовов функций (pipe);
- Асинхронное программирование async/await;
- Кэширование результатов функций, через параметр memorize;
- Лямбда цепочки;
- Концепция обмена сообщениями между частями системы;
Более подробно о возможностях я распишу в документации.
BAYRELL Language содержит Runtime библиотеку, которая позволяет разрабатывать Full Stack программы. Начиная от запросов к базе данных, работа с API, заканчивая компонентами и рендером на клиенте.
Как начать пользоваться языком
Я постарался создать базовую документацию, и буду ее потихоньку дополнять. Проект находится в альфа стадии, и возможно функционал некоторых классов будет изменен в будущем.
Установка языка осуществляется следующей командой:
npm install -g bayrell-lang-compiler
Чистый проект:
git clone https://github.com/bayrell-tutorials/clear-project
cd clear-project
git submodule init
git submodule update
Трансляция проекта:
bayrell-lang-nodejs make_all
bayrell-lang-nodejs make_symlinks
Наблюдение изменений в проекте и автоматическая трансляция:
bayrell-lang-nodejs watch
Чтобы посмотреть проект, нужно в nginx прописать document root к папке web в проекте.
Ссылки на проекты примеры:
HTML Шаблонизатор
У шаблонизатора несколько отличительных особенностей:
- Транслятор конвертирует код шаблона в PHP и JS.
- Можно создавать свои компоненты и переиспользовать их в различных частях системы или в других проектах.
- Direct патчинг HTML DOM вместо Virtual DOM.
Более подробное описание работы шаблонизатора, с примерами работы можно найти в документации.
Если вкратце, то создается модель данных LayoutModel, которая содержит всю информацию о странице (модель всего шаблона). Модель страницы — неизменяемая. Каждый компонент содержит чистую функцию render, которая отображает HTML код на основе модели. Модель может изменяться через JS события (нажатия кнопок мыши, клавиатуры и т.п.) путем создания новой модели с новыми свойствами. При возникновении события, компонент передает событие вышестоящему компоненту, о том, что его модель изменилась. Вышестоящий компонент, изменяет свою модель и передает сообщение выше, пока не дойдет до RenderDriver.
RenderDriver изменяет LayoutModel, и запускает перерисовку через requestAnimationFrame. Функция отрисовки использует функции render у компонентов, чтобы пропатчить HTML DOM. В функции render не создается VirtualDOM, а происходит прямой патч HTML.
Функциональное программирование
Сейчас тренд на функциональное программирование (ФП). Когда я разрабатывал язык, я понял, что такое функциональное программирование. Это, наверное, самый важный инсайт был для меня. Я понял, что ФП, это не Lisp, Haskell, F#, а принципы, которые применимы как к ФП, так и ООП языкам.
В своей практике я начал применять функциональный подход. И обнаружил, что исчезают некоторые шаблоны проектирования, такие как фабрики, контейнеры, Dependency injection, потому что, они становятся не нужны. Код становится чище, понятнее и его поведение легче предсказать. Уходит избыточность кода и файлов.
Например, у меня, после введения неизменяемых типов данных (структур), исчезла необходимость писать private у переменной, и создавать функции setName (), getName (). Обычно в ООП их всегда пишут. А в функциональном программировании они не нужны. Потому что структура неизменяемая.
Бонусом к функциональному программированию является то, что для него легко писать юнит тесты. Чтобы написать юнит тест, нужно подать в функцию входные данные и сравнить результат с правильным. А весь код состоит из функций, что облегчает задачу для юнит тестирования.
Имхо, но чистый код, возможен только с применением функционального подхода.
Принципы функционального программирования
Данные принципы относятся к языку BAYRELL Language. В других ФП языках некоторых принципов может и не быть.
Первый принцип — это способ написания функций, которые можно запустить на разных серверах или потоках (масштабировать). При этом масштабирование не влияет на результат этих функций. Функции могут работать параллельно, а их работа не влияет друг на друга. Другими словами, масштабирование, это как кассы в супермаркете. Каждая касса определяется алгоритмом, если нагрузка увеличивается, открываются несколько дополнительных касс, если нагрузка низкая, уменьшается количество касс. Сами кассы идентичны и работают независимо друг от друга. Покупатель может подойти к одной из касс, и расплатиться за товары.
Второй принцип — отсутствие состояния. Функция не хранит в себе состояний, и изменяемых данных. Не сохраняет файлы и сессии локально, чтобы их потом использовать. Два последовательных запроса от клиента могут отправиться к разным экземплярам функции, и эти экземпляры функции должны одинаковым образом обработать запросы от клиента.
Состояния лучше хранить во внешних системах: база данных, memcached и т.п. В 12 факторной модели он называются сторонние службы.
Третьим принципом функционального программирования, является возможность создания асинхронных функций. Что это значит? Пример, из жизни. Пусть нам надо написать письмо человеку с каким-то предложением, и узнать его ответ. А после его ответа принять решение. Но пока человек, отвечает, думает, он может отвечать, допустим сутки, в это время, мы можем заняться другими делами, пойти в кино, сверстать сайт, и т.п. Очень важно, что задачу выполняет один человек, но он делает «параллельно» эти задачи.
В программировании также. Когда происходит запрос к базе данных, к другому сервису, чтение с диска, можно запустить другую вычислительную функцию. А когда придет результат, от базы, то продолжить выполнение первой функции, с того момента, где остановились. И при этом не обязательно заводить отдельные потоки на каждую функцию. Можно выполнять всё в одном потоке. Это называется кооперативная многозадачность. Кооперативная многозадачность работает в пределах одного потока, одного исполнителя.
Четвертый принцип, это применение неизменяемых структур данных. Неизменяемые структуры данных — это данные, которые нельзя поменять. Чтобы их поменять, нужно создать новый объект, с новыми значениями.
У неизменяемых структур данных три важных свойства:
- Чтобы узнать изменились ли данные, или нет, достаточно сравнить ссылку на объект. Если ссылка изменилась, значит это новый объект, и данные изменены.
- Их можно использовать в многопоточных системах, и не боятся, что какой-то поток, изменит объект, пока другой им пользуется. Можно обойтись без мьютексов.
- Если передавать неизменяемые объекты в функции, то можно не бояться, что функции изменят значения объекта. Тем самым алгоритм становится значительно проще. И ошибок гораздо меньше.
Я скажу, что необязательно везде использовать неизменяемые структуры данных. Например, внутри функции, можно использовать обычный массив, а затем на выходе, результат функции (массив) превратить в неизменяемый.
Пятый принцип функционального программирования, это чистые функции.
Чистая функция — это:
- Всегда возвращает одинаковый результат, на одни и те же входные данные.
- Не изменяет входные данные.
- Не обращается к базе, диску и прочим внешним источникам данных.
- Не является асинхронной.
- Не работает с глобальными переменными.
Очевидно, что результат выполнения чистых функций можно кэшировать. Если одни и те же входные данные, то достаточно один раз вычислить результат этой функции, и дальше его использовать. Оптимизация HTML рендера может использовать данное свойство, если функция рендера чистая, а модель данных страницы неизменяемая.
Шестой принцип — каррирование и функции высших порядков. Функция высшего порядка — это функция, которая возвращает или принимает в качестве аргумента другую функцию. А каррирование, это метод создания функций высшего порядка. Обычно каждая вложенная функция принимает один аргумент. Но это не обязательное условие.
Пример на языке BAYRELL Language:
/**
* Sum a + b
*/
lambda int sum(int a) => int (int b) use (a) => a + b;
или анонимная функция:
fn sum = int (int a) => int (int b) use (a) => a + b;
Функции высших порядков удобно применять в map, reduce, array_filter (php), цепочках pipe.
Седьмой принцип, это цепочки pipe. Это такой способ написания вызова функций, когда результат предыдущей функции попадает на вход следующей. С pipe можно познакомится в Linux консоли.
Например:
ls -la | grep .txt
Это pipe. Он выводит список файлов с расширением txt. Сначала вызывается функция, которая выводит листинг всех файлов, затем вызывается фильтр, который оставляет только txt.
В BAYRELL Language можно тоже создавать pipe. Пример:
User user = new User()
|> User::setName('John')
|> User::addEmail("john@example.com")
;
Или проще:
User user = new User()
-> setName('John')
-> addEmail("john@example.com")
;
В данном случае создается пользователь, при вызове addEmail, первым аргументом передается объект пользователя и меняется имя, а вторая функция, добавляет email «john@example.com» и возвращает новый измененный объект. Сама передача объекта не указывается, поэтому функции содержат один аргумент, а не два. Это сделано для облегчения читаемости кода.
Pipe может быть асинхронным.
Более подробные примеры в документации.
Функциональное программирование очень хорошо подходит для разработки облачных систем. Есть даже целое направление FaaS — функция как сервис.
Концепция обмена сообщениями
Сейчас в BAYRELL Language обмен сообщениями работает во Frontend: события JavaScript, нажатие мыши, события компонентов. И отправка Ajax запросов на Backend.
Пример отправки сообщения:
MessageRPC msg = await @->sendMessage
(
new MessageRPC
{
"api_name": "App",
"space_name": "ApiInterface",
"method_name": "getDocument",
"data":
{
"document": "/ru/" ~ prefix,
},
}
);
Document document = msg->isSuccess() ? msg.response : null;
К сожалению, REST Api не полностью может покрыть необходимый функционал. REST это всего лишь CRUD. А иногда, требуется запускать запросы, которые, например, компилируют какой-то модуль, загружают файл на сервер, или отправляют уведомления о событиях. И использовать методы HTTP (GET, POST, PUT, DELETE) для передачи команды запроса не очень хорошая идея.
Поэтому, при разработке концепции обмена сообщениями, была взята концепция D-BUS за основу. Есть объект, который получает сообщения. У него есть некий интерфейс, а у интерфейса метод.
Интерфейсы позволяют стандартизовать обращения. Например, CRUDInterface. Если объект обладает этим интерфейсом, то все веб компоненты, в которых реализован этот интерфейс, смогут отображать данные из этого объекта. И свободно обращаться в этому объекту по API.
Работу обмена сообщениями планируется в будущем сделать, через различный транспорт: WebSocket, DBus, RabbitMQ. Пока реализован транспорт через Ajax в качестве вызова API.
О языках
Я понял, во время разработки языка, что концепции языков С++, C#, Python, Java, PHP похожи. Отличается синтаксис, комьюнити и библиотеки, под эти языки.
Для себя я определил следующие правила, к которым нужно прислушиваться при выборе языка или фреймворка:
- Размер сообщества и наличие обилия библиотек. Писать все с нуля самому сложно. Я написал 4 фреймворка, и теперь понимаю, что порой лучше взять готовые решения.
- Оценить будущие затраты на сопровождения и доработку кода, который вы написали.
- Легко ли найти программистов под этот язык или фреймворк, и захотят ли они с ним работать. И сколько они будут просить зарплаты.
- Сможете ли вы найти решение возникшего вопроса в интернете за несколько минут, касательно выбранной технологии?
- Выдерживает ли этот язык или фреймворк нагрузку, которая предполагается при размещении в продакшн.
Каждый язык, фреймворк хорош по своему, и все зависит от ситуации. И перед выбором технологии нужно оценить риски. А затем принимать решение.
Будущие направления развития языка BAYRELL Language
То, зачем я начал разработку, это интеграция кода написанных на разных языках (Java, Python, C#, C++, NodeJS, JavaScript, Go, Lua). Или разработка библиотек под эти языки программирования. В рамках создания BAYRELL Language, я планирую максимально интегрировать его с этими языками. В идеале, я вообще задумываюсь, о свободной конвертации программ из одного языка в другой. Языки похожи, но они имеют свои различия и нюансы. Из-за этого транслятор из одного языка в другой создать сложно.
Я изучал данную проблему переиспользования исходного кода, в разных языках. Это называется Language Binding. В компилируемых языках есть dll, и все решается просто. Но появились интерпретируемые языки, и использование кода из одного языка другим, становится проблемой.
Решения, которые я нашел для интерпретируемых языков, лежат в области микросервисной архитектуры, поддержания библиотек сразу на нескольких языках или разработки трансляторов языков.
Еще одним направлением, является развитие FaaS системы и визуального программирования.
Это разработка мышкой функциональных программ в облаке. Я как программист очень много времени трачу на печатание кода, на постоянные сборки и компиляции. Хотя нужно тратить время на создание алгоритмов. Мне нравятся конструкторы сайтов, автоворонок продаж, чат ботов. Где можно мышкой потыкать, и получить готовую рабочую версию. Только зачастую, в некоторых конструкторах, нельзя писать кастомный код. Либо конструктор не дает выгрузить разработанный код. И еще в конструкторах нет Git.
Визуальное программирование сокращает время на создание программы. Так, к примеру, создать лэндинг на конструкторе можно за 2–3 дня. А если делать макет, дизайнить и верстать, то уйдет месяц, а то и больше.
Более подробно TODO List я написал в документации
Итог
Я понимаю, что я многого могу не знать, и где-то мне не хватает опыта, поэтому буду благодарен вашим комментариям.
По вопросам сотрудничества и вступления в сообщество пишите в ЛС.
Буду рад, если подпишитесь на меня в соц сетях.
Ссылки на соц сети доступны на странице.