В 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 запрещены), то есть задача создания хорошего и быстрого линтера кажется не такая уж непосильная.
Комментарии (6)
art1z
27.06.2022 20:25C var понятно, ему место в истории, а let то чем не угодил? Все переводить на тру функциональщину и immutable.js? Так все равно останутся безнаказанно мутируемые аргументы фукций
xyli0o Автор
27.06.2022 21:17:) Мутации легко добиться и с "конст":
const d = {k: 1}; d.k = 2
но это в локальном скоупе функцию. А вот аргументы функции лучше не мутировать -- этот скоуп на уровень выше.Кейворд let он как бы режет глаза :) Наиболее частое использование let на моей памяти -
let x; try { x = guessNumber() } catch() { x = 42 } ...
-- тут как бы не юзать let и вынести try в функцию.
Kelbon
А причем тут концепты и модули-то?
xyli0o Автор
Без них было бы совсем печально :(