Концепты и модули cpp. Взгляд сo стороны undefined разработчика

от автора

В 2002 году Алекс А. Степанов проводит лекцию в адоби: STL and Its Design Principles — где упоминает кейворд concept (там прям целый слайд про концепты). В 2009 году в свет выходит книга Elements of programming (Stepanov, McJones) и где по-моему нет ни одного алгоритма без концептов. В 2011 новый стандарт языка с++11, где в отложенных (прям очень жаль) фичах фигурируют концепты. В 2014 мир видит творенье Страуструпа — Tour of C++, где глава 5.4 названа Concepts and Generic Programming, хотя язык не поддерживает кейворд concept. Годом ранее, в 2013, Андрю Саттон публикует бумагу Concepts Lite. В стандарте с++14 появляется новая фича digit separators, но нет концептов. В 2017 на реддите обсуждают c++17 и предлагают отдохнуть еще три года.

Через три года сочувствующий html разработчик захочет попробовать язык с чистого листа, заюзав и концепты, и модули. Модули привносят важное свойство — уменьшение времени компиляции. Идея в шаг за шагом написании небольших компонентов и комбинировании их после в нечто осознаное, например, линтер.

Первым делом добавим функцию print:

// guard for non-printable types template<typename T, typename... Ts> void print(const T& x, const Ts&...) {   auto msg = "\n[error] not supported print operation for type ";   std::cout << msg << typeid(x).name() << "\n"; }  // print first arg and continue with rest template<Printable T, typename... Ts> void print(const T& x, const Ts&... rest) { ... }

Компонент для матчинга строки нашего линтера (javascript кода) будет выглядить как-то так:

// checking for pair of quotes, while skipping whats inside them template<InputIterator I> constexpr result_t lint(...) {     auto pred = [](char ch){ return ch == '\'' || ch == '"'; };     ... }

Так как алгоритм выше constexpr, запускаем тесты на каждую! компиляцию модуля:

constexpr auto test(const std::string_view str) {    auto p = std::make_pair(std::begin(str), std::end(str));    return !lint(..., p) && p.first == p.second; }  static_assert(test("'foo'"), "single quoted string test"); static_assert(test("'\\' quoted'"), "string with escaped single quote test");

У линтера, кажется, вырисовывается интерфейс:

result_t lint(..., /* pairs of iterators */) { }

Тут идея вполне очевидная: много компонентов с одинаковым интерфейсом. Можем решить ее с помощью наследования, а можем позаимствовать идею из видео Better Code: Runtime Polymorphism (Sean Parent) — в двух словах, тот же самый полиморфизм, но удобнее (e.g. extension of built-in types). Продолжим:

struct keyword_t { std::size_t id; }; template<InputIterator I> constexpr result_t lint(const keyword_t& x, matcher<I>& m) { ... }  // models forall relation // if at least one fails -> all fail template <InputIterator I> constexpr result_t lint(const std::vector<linter>& xs, matcher<I>& m) { ... }

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

template <InputIterator I>  struct matcher {     I first;      I last;     constexpr matcher(I first, I last) : first{first}, last{last} {}      // advance 'first' while predicate is hold     template <Predicate<value_t> Op>     constexpr void skip(Op pred) {         first = std::find_if_not(first, last, std::move(pred));     }     ...

Есть несколько вариантов для типа возвращаемого значения result_t. У линтера на самом деле простая задача и, следовательно, простой результат действия: или мы нашли несоответствие, или все ок. По идее, выбор падает на bool, но на практике интересен линтер чуть-чуть поумнее, например std::optional\<std::size_t\>. Если все ок, возвращаем пустой опшионал, в противном случае — количество проделанных шагов до несоответствия.

Если линтер нашел недействительный токен или не нашел искомый, кидаем эксепшн:

struct panic_t { }; template <InputIterator I> constexpr result_t lint(const panic_t&, matcher<I>& m) {     return 0         ? result_t{}         : throw std::domain_error("\n err near \n-> " + m.debug_info()); }  struct iff_t { linter a; linter b; }; // models relation between pair of linters // if linter 'a' not fails, linter 'b' must not fail template <InputIterator I> constexpr result_t lint(const iff_t& x, matcher<I>& m) {     auto lhs = lint(x.a, m);     if (lhs.has_value()) return lhs;     auto rhs = lint(x.b, m);     return rhs.has_value() ? lint(panic_t{}, m) : result_t{}; }

Компоненты нашего линтера захочется композировать (или компоновать), поэтому пару/тройку связующих:

struct or_t { linter a; linter b; }; template <InputIterator I> constexpr result_t lint(const or_t& x, matcher<I>& m) {     return lint(x.a, m) ? lint(x.b, m) : result_t{}; }  struct optional_t { linter a; }; template <InputIterator I> constexpr result_t lint(const optional_t& x, matcher<I>& m) {     lint(x.a, m);     return {}; }  // models forall relation // if at least one fails -> all fail template <InputIterator I> constexpr result_t lint(const std::vector<linter>& xs, matcher<I>& m) {     for (const auto& x : xs) {         auto r = lint(x, m);         if (r.has_value()) return r;     }     return {}; }

Теперь собственно к разборке («линтингу») javascript кода. Хедеры (или импорты) javascript файлов выглядят примерно так:

import * as foo from "../foo" import bar, { boo, kung as fu, mu } from "../bar";

Грамматика хедеров тут: https://262.ecma-international.org/6.0/#sec-imports .
Компонент для хедеров import_t сигнатурой будет очень похож на все предыдушие компоненты:

struct import_t{}; template<InputIterator I> result_t lint(const import_t&, matcher<I>& m) { ... }

Внутри хотя будет интереснее, так как грамматику можно описать композицией компонентов (тут прям нужно добавить, что описание декларативное без всяких while и for-loop 🙂

// match: * as foo auto namespace_import = iff_t{     char_t{context::Chars::Star},     std::vector<linter>{         keyword_t{context::Keywords::As},         identifier_t{}     } };  // match: { foo, bar as boo, ... } auto named_import = iff_t{     char_t{context::Chars::LeftCurlyBracket},     std::vector<linter>{         optional_t{import_var_list_t{}},         char_t{context::Chars::RightCurlyBracket},     } };  // match: foo | foo, * as bar | foo, { bar... } auto imported_default_binding = std::vector<linter>{     identifier_t{},     optional_t {         iff_t {             char_t{context::Chars::Comma},             or_t{                 namespace_import,                 named_import             }         }     } };  // and combining all togather  auto rule = iff_t{     keyword_t{ context::Keywords::Import },     or_t{         std::vector<linter>{             string_t{},             optional_t{char_t{context::Chars::Semicolon}},         },         std::vector<linter>{             or3_t {                 namespace_import,                 named_import,                 imported_default_binding,             },             keyword_t{context::Keywords::From},             string_t{},             optional_t{char_t{context::Chars::Semicolon}},         }     } };

Описание import_t моделирует граф. Если в графах грамматики встречается цикл, уносим его в алгоритм, другими словами, для удобства добиваемся direct acyclic graph. Примером цикла есть токен import_var_list_t выше, который матчит { foo, bar, …}, алгоритм которого будет петля, которую тоже можно тестировать на каждую компиляцию.

Как-то не хочется заключение писать, так как линтер еще ой как далек от завершения. Хочется вспомнить книгу JavaScript: The Good Parts (Douglas Crockford, 8 May 2008), которая, как видно из названия, предлагает юзать только «хороший» сабсет грамматики из «яваскрипта». На сегодняшний день хороший сабсет будет еще меньше (например, for-loop, while — нежелательны; var, let запрещенны), то есть задача создания хорошего и быстрого линтера кажется не такая уж непосильная.


ссылка на оригинал статьи https://habr.com/ru/post/673388/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *