Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Не нравится ООП? Делайте свой язык программирования!

24 комментария

В интернете нынче модно говорить что «ООП это шлак», и многие мечтают сделать свой собственный язык программирования но чего-то боятся. А на самом деле, если подойти с умом, тут все просто. Серьезно! Я знаю, вы хотите мне возразить, что дескать…

  • Писать свой компилятор в native code/IL/bytecode слишком сложно — а и не надо!! Вы понимаете, что существующие компиляторы вроде С или Java оттачивались годами, сотнями людей? Почему бы не воспользоваться всем этим богатством, компилируя ваш язык в тот же С/C++, и потом получая от С/C++ компилятора все оптимизации и плюшки?

  • Меня не прет идея возиться с кастомными форматами файлов для лексинга и парсинга, это депрессивно — а и не надо!! Просто возьмите фреймворк, который поддерживает парсеростроение прямо в коде.

  • У меня сейчас весь код на Java/C#/C++ написан, как я сделаю interop? — да очень просто, ведь с подходом транскомпиляции, вы можете транслировать свой язык в любой из вышеперечисленных, генеря и потом потребляя любой интерфейс, причем в обе стороны.

  • Язык-то я сделаю, а как насчет поддержки языка в моей любимой IDE? — о, ну это уже высший полет. Для начала, постарайтесь сделать лаконичный маленький язык, для которого инструменты не критичны. А потом можно научиться и тулы поддерживать (или писать свои).

Ну что, убедил?

Чтобы показать насколько это все просто, вот небольшой пример: представим, что вы хотите расширить определение С-образной глобальной функции, добавив следующие фичи:

  • Определить свой набор «коротких» типов переменных, вроде i32 или f64 на замену int и double.

  • Передавать аргументы в формате x,y : i32, то есть переиспользуя определение типа для нескольких сразу.

  • Добавлять в тело функции определения переменных вроде x = 5 так чтобы, при условии что x не имя параметра, это преврашалось в полноценную декларацию переменной, а иначе просто присваивалось значиние.

Для начала такой фичесет подойдет? Я знаю что мало, но я тут и не пытаюсь целый язык сделать. Вот как это будет выглядеть:

Наш язык… …станет вот этим
void foo(x,y:i32, z:f64)
{
  x = 5;
  w = 123;
}
void foo(int32_t x, int32_t y, double z)
{
  x = 5;
  int w = 123;
}

Структуры для языка

Во всех языках есть механизмы построения парсеров. Я возьму C++ и Boost.Spirit, для примера, но вообще язык тут особого значения не имеет. Для начала давайте сделаем новые типы вроде f32 вместо float:

struct numeric_types_ : qi::symbols<wchar_t, wstring>
{
  numeric_types_()
  {
    add(L"i32", L"int32_t");
    add(L"f32", L"float");
    add(L"f64", L"double");
  }
} numeric_types;

Теперь определяем функцию:

struct function
{
  wstring name;
  vector<parameter> params;
  vector<assignment_statement> assignments;
 
  // поиск параметра по имени; реализация банальна
  boost::optional<const parameter&> find_parameter(const wstring& name) const;
};

У любой функции есть имя, она берет сколько-то там параметров (ну, деклараций параметров, но краткость сестра таланта), и у нее есть тело, которое в нашем случае будет состоять исключительно из присвоений значений переменным (не очень практично, I know).

Поскольку мы умеем определять аргументы в стиле x,y:i32, т.е. несколько с одним типом, собственно структур parameter мы определим вот так:

struct parameter
{
  vector<wstring> names;
  wstring type;
};

Ну и наконец присвоение значений переменным можно сделать вот так:

struct assignment_statement
{
  wstring variable_being_assigned;
  wstring value;
  wstring infer_type() const
  {
    if (value.find(L'.') == wstring::npos)
      return L"int"s;
    return L"float"s;
  }
};

Выше показан г~код для определения типа, думаю вы понимаете что в последствии это можно пофиксить.

Всё, структуры готовы, можно строить парсер. (На самом деле есть еще этап их адаптации через Boost.Fusion, но это implementation-specific деталь, если что гляньте в сорцы.)

Парсер

Парсер для нашего языка написать настолько легко что я просто приведу весь код целиком, а потом мы его обсудим, ок?

template<typename Iterator>
struct function_parser : qi::grammar<Iterator, function(), space_type>
{
  function_parser() : function_parser::base_type(start)
  {
    using qi::lit;
    param %= 
      +alnum % ','
      >> ':'
      >> numeric_types
      >> -char_(',');
    assignment %=
      +alnum
      >> '='
      >> +alnum
      >> ';';
    start %= lit("void ")
      >> +(char_ - '(')
      >> '('
      >> *param
      >> ')'
      >> '{'
      >> *assignment
      >> '}';
  }

  qi::rule<Iterator, parameter(), space_type> param;
  qi::rule<Iterator, assignment_statement(), space_type> assignment;
  qi::rule<Iterator, function(), space_type> start;
};

Для начала, вот эти qi::rule просто говорят парсеру как то что он распарсит ложится на структуры что мы определили ранее. Например, вот хочется распарсить присваивание вроде x = 3, что это? Это идентификатор (то есть, 1 и более alphanumeric символов), потом =, потом еще раз набор символов и в конце ;.

Конкретно в Boost.Sprit, в отличии от регулярок, «один и более» записывается как + до типа символа, т.е. +alnum. То есть + означает «один и более», * — «сколько угодно», и так далее. Вот и получается что присваивание мы распарсили, а поскольку наш qi::rule мэпит его на assignment_statement, поля этой структуры будут присвоены автоматически. Это гениально, или как?

То же и с другими частями языка. Хочешь распарсить несколько переменных через запятую и запихнуть их в вектор? Пишем +alnum % ',' где оператор % – это как сказать *(+alnum >> ','), только короче. Что тоже удобно.

Так вот, парсер у нас готов, можно парсить. На Spirit это делается вот так:

template<typename Iterator>
wstring parse(Iterator first, Iterator last)
{
  using boost::spirit::qi::phrase_parse;
  function f;
  function_parser<wstring::const_iterator> fp{};
  auto b = phrase_parse(first, last, fp, space, f);
  if (b)
  {
    return render(f);
  }
  return wstring(L"FAIL");
}

…где render() – это функция которая обходит то что мы напарсили и генерит из этого чистейший, готовый к компиляции С (никто не мешает вам выводить сразу в N разных языков).

Pretty print

Короче, мы попарсили функцию, получили из нее ООП структуры, ну и теперь можно из них чего-нибудь нагенерить. Для этого их нужно обойти, что в нашем конкретном случае не сложно. Сначала пишем название функции:

inline wstring render(const function& f)
{
  wostringstream s;
  // name of the function (assume void-returning)
  s << "void " << f.name << "(";

Потом выводим параметры, не забывая что у нас может быть несколько имен для одного и того же типа аргумента:

// each of the parameters
const auto param_count = f.params.size();
for (auto i = 0u; i < param_count; ++i)
{
  auto& p = f.params[i];
  for (int j = 0; j < p.names.size(); ++j)
  {
    s << p.type << " " << p.names[j];
    if (j + 1 < p.names.size()) s << ", ";
  }
  if (i + 1 < param_count) s << ", ";
}
s << ")\r\n{\r\n";

А потом, аккуратненько, присваивания. Не забываем что тип нужно прописывать только если переменная не фигурирует где-то в параметрах функции:

// each of the assignments
const auto assign_count = f.assignments.size();
for (auto i = 0u; i < assign_count; ++i)
{
  s << "  ";
  auto& a = f.assignments[i];
  auto type = a.infer_type();
  bool is_param = f.find_parameter(a.variable_being_assigned) != boost::none;
  if (!is_param)
    s << type << " ";
  s << a.variable_being_assigned << " = " << a.value << ";\r\n";
}
s << "}";
return s.str();

Вот собственно и всё. Тут в принципе можно реализовать «полноценный» Visitor, если хочется.

Заключение

Как вы поняли, тут остался шаг компиляции полученного кода — думаю всем итак очевидно как это делать, это зависит от языка который вы нагенерили. Вообще я ратую за «портативный» C/C++, но решать в конечном счете вам.

Если хотите сорцы проекта, они тут. Мой пример на С++, но вы можете реализовать свой язык на чем угодно. Мораль в том что создать сейчас свой кросс-компилируемый язык легко, поэтому вместо того чтобы ныть про ООП и воевать с ветряными мельницами, проще сесть и запилить что-то своё. Так что садитесь и пишите спеку вашего чудо-юдо языка. Удачи!

Advertisements

Written by Dmitri

2 сентября 2016 в 16:10

Опубликовано в Programming

Tagged with , ,

комментария 24

Subscribe to comments with RSS.

  1. Навеяно Егором Бугаенко)?

    Николай

    2 сентября 2016 at 16:56

    • Ага, вы угадали.

      Dmitri

      2 сентября 2016 at 16:59

      • Как раз тоже хотел спросить, не Егор ли здесь шуму наделал. :) Мне кажется, с Егором спорить бесполезно, потому как он твёрдо стоит на своём, и несогласных (а у него в блоге таких полно, и они преимущественно пишут развёрнутые комментарии. особенно некий David Raab) скорее назовёт хейтерами, чем признает, что ошибался даже в очевидных вещах. Честно говоря, мне сложно поверить, что у него есть последователи, и я почти всегда был уверен, что он просто тролляка (и мне). :) Но, оказывается, это на полном серьёзе, и почти что радикальная маргинальность его точки зрения не стоят такого ажиотажа.

        >> Так что садитесь и пишите спеку вашего чудо-юдо языка.
        Егор бы запилил свой D29 наконец-то. :D

        shell

        2 сентября 2016 at 18:05

        • Ну, на мой комментарий ответа не последовало. Твердо на своем стоят и еговисты, но их догма яблока выеденного не стоит. Прижерживаешься своей особенной точки зрения? Будь готов ее отстаивать.

          Да, и что такое D29?

          Dmitri

          2 сентября 2016 at 19:20

        • Не вижу линка «Ответить», поэтому здесь. Идея Егора о языке+платформе, которые включали бы все аспекты современной разработки , и перестали бы походить на Cobol и C. Чудная затея, но некоторые аспекты мне кажутся весьма разумными. Линка оставлять не буду, потому что сабж находится поисковиками без проблем.

          shell

          2 сентября 2016 at 19:55

        • Нашел вот тут.

          Dmitri

          2 сентября 2016 at 20:01

        • там в фичах почти половина английского словаря ))
          рефлексию тоже приговорили

          Podgorodnichenko Denis

          2 сентября 2016 at 20:22

        • Да, это какая-то клиника.

          Dmitri

          2 сентября 2016 at 23:28

  2. да егор зажег ))

    Podgorodnichenko Denis

    2 сентября 2016 at 19:45

  3. кстати про эти все замечательные идеи с DSL и штоб сразу в дотнет\плюсы его. Помнится мне был такой Nemerle, а потом парни из JetBrains делали немерле 2 который превратился в нитру. не слышно давно про него..

    Podgorodnichenko Denis

    2 сентября 2016 at 23:18

    • Нитра, к сожалению, умерла. Проект не выстрелил, люди которые ее делали вроде уже в JB не работают. Сам проект вроде лежит где-то в опенсорсе.

      Dmitri

      2 сентября 2016 at 23:29

      • Увы JB решили что им проект не нужен так как есть другие.
        Ну и у некоторых плохо выходит пиарить, а точнее даже делает антирекламу.

        NN

        3 сентября 2016 at 13:23

        • На самом деле, маркетинг в JB часто делается руками самих же авторов. Посмотрите как активно тов. Бреслав пиарит Котлин! А чтобы Немерловцы ездили по международным конфам и показывали свой продукт я что-то не припомню.

          Dmitri

          3 сентября 2016 at 20:11

        • NN

          3 сентября 2016 at 23:28

        • Ага. Только это доклад по Nemerle а не по Nitra. За Nitr-ой стояла серьезная компания, продуктам которой доверяют миллионы программистов. Там можно было пиарить идеи так же как и Котлин. А может нельзя было? ХЗ, может все это было NDA, хотя не похоже.

          Dmitri

          3 сентября 2016 at 23:36

        • Про Nitra да, там плохой подход был с рекламой продукта.
          С другой стороны не было и человека который был выделен для этого.

          NN

          3 сентября 2016 at 23:39

        • Дык, как показывает практика, и сами разработчики могут пиарить технологию. Необязательно везде иметь выделенного евангелиста. Более того, отдельно под Nitra искать человека было бы наивно, а у текущих евангелистов с нагрузкой итак было все в порядке.

          Dmitri

          3 сентября 2016 at 23:45

      • Boo не дожил до вменяемого инструментария.
        Nemerle не дожил до вменяемого инструментария.
        Nitra не выстрелила, даже и не пытаясь стать чем-то особо удобным.
        А вот Kotlin чуть ли не начался с инструментария, и, как я понимаю, вполне себе выстрелил. (Я пробовал немного, меня восхищает его smooth user experience, так сказать.)

        Кажется, выводы очевидны. Или я где-то ошибся?

        Maxim Kamalov

        3 сентября 2016 at 18:04

        • Непонятно какие выводы можно сделать. Nitra был интересным, с точки зрения, парсеростроения проекта. Основная идея была соединить вместе парсер/лексерную инфраструктуру и инфраструктуру нужную для ReSharper чтобы производить и показывать анализы. Некоторые примеры Nitra, в которых она могла вывести логически пропущенный токен и предложить возможные варианты — это было весьма science fiction по сравнению с существующими анализаторами.

          C языком Воо проблема одна: там кроме грамотного метапрограммирования не было толком фич. А метапрограммирование никто не тянят кроме избранных. В Nemerle тоже сделано метапрограммирование, собственно отчасти на нем и строились DSLи для Nitra, чтобы вместо привычных lex/parse файлов со своими специфичными форматами (R# правда для них языковые сервисы тоже сделал, что мегакруто) можно было писать на обычном языке.

          Kotlin — очень прагматичен. В нем есть интересные фичи, в частности smart casts, аналог методов расширения, проперти которых даже в Java нет, и в целом на нем можно писать. Я бы не сказал что у него есть прорывные фичи: он скорее просто делает Java более удобной, что безусловно очень хорошо. За ним — достаточно сильный маркетинг, плюс у него хитрое позиционирование (он совместим с Java, и их можно смешивать в одном проекте), ну и конечно инструментарий тоже неплохо иметь, благо ООП.

          С другой стороны, если делать язык с front-end’ом, то варианта делать язык без инструментария нет вообще, именно от инструментария и нужно отталкиваться.

          Dmitri

          3 сентября 2016 at 20:10

        • Мой вывод такой: выкатывать язык с убогим инструментарием — помогать ему не выстрелить. Если и сключения: Go хорошо пошел среди ненавистников кликов мышью и графических интерфейсов. А вот Nemerle и Boo пытались снискать любовь именно у людей, привыкших к анализаторам, рефакторингам и саморасставляющимся скобкам.

          Maxim Kamalov

          3 сентября 2016 at 23:15

        • Ну, возможно. Это одна из причин, почему мне нравится идея делания языка с front-end’ом: в этом случае у тебя нет варианта прошляпить тулы. Хотя сейчас мы так развращены современными IDE что ожидаем от них миллиона фич на старте.

          Dmitri

          3 сентября 2016 at 23:39

        • судя по форуму нитра все еше более чем жива, и даже с инструментарием там все ок
          http://rsdn.org/forum/nemerle/

          Podgorodnichenko Denis

          4 сентября 2016 at 1:29

        • КТ>Поэтому я говорю: Нитру не удалось продать внутри Jetbrains’а самому Jetbrains’у.

          По тому что мы не продавцы, а инженеры.
          Наша работа не продавать, а создавать.

          И вот тут мы находим главный продолб. Котлиновцы мало того что сами по всему миру на конференции летали, они еще и воркшопы внутри компании делали (сам присутствовал).

          Dmitri

          4 сентября 2016 at 11:23

        • согласен, про котлин слышали чуть менее чем все, а про нитру…

          Podgorodnichenko Denis

          5 сентября 2016 at 13:24


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: