- PVSM.RU - https://www.pvsm.ru -
Что, если бы мы умели располагать атомы один за другим как нам угодно?
Ричард Фейнман
Сколько парадигм программирования вы сможете назвать? Список на этой странице википедии [1] содержит ни много ни мало 76 наименований. Этот список может быть пополнен ещё одним подходом, названным Design by Introspection. Его основная идея состоит в активном использовании простых средств метапрограммирования и интроспекции типов (времени компиляции) для создания эластичных компонентов.
Автором данного подхода является Андрей Александреску [2]. В статье были использованы материалы из его выступления [3] на DСonf 2017.
В 2001 году в книге «Современное проектирование на С++» был представлен паттерн под названием policy-based design. В целом, это паттерн «стратегия», но использующий шаблоны и собирающийся во время компиляции. Шаблонный класс host принимает своими параметрами набор типов policy, реализующих каждый какую-то независимую функциональность, и внутри себя использует интерфейсы, предоставляемые этими компонентами:
struct Widget(T, Prod, Error)
{
private T frob;
private Prod producer;
private Error errPolicy;
void doWork()
{
// используем неявные интерфейсы
// обычный duck-typing
}
}
Здесь шаблон описан коротким синтаксисом. (T, Prod, Error)
— его параметры.
Инстанцирование выглядит так:
Widget!(int, SomeProducer, SomeErrorPolicy) w;
Плюсы очевидны: эффективность шаблонов, хорошее разделение и повторное использование кода. Однако компоненты являются цельными, неразборными. Если отсутствуют какая-то часть интерфейса, то это приведёт к ошибке компиляции. Попробуем развить эту схему, придать компонентам «пластичность».
Итак, для этого нам понадобится:
Давайте взглянем, какие средства языка D можно использовать по каждому из пунктов:
.tupleof
, __traits
, std.traits
__traits
– встроенное в компилятор средство рефлексии. std.traits
– библиотечное расширение встроенных трейтов, среди которых нас будет интересовать функция hasMember
.static if
, static foreach
static if
, static foreach
– это if
и foreach
времени компиляции.Важнейшим свойством Design by Introspection являются опциональные интерфейсы. Здесь компонент содержит R обязательных примитивов (может быть 0) и O необязательных. С помощью интроспекции можно выяснить, задан ли определённый примитив, причём знание о пропущенных примитивах так же важно, как и о тех, которые компонент содержит. Число возможных интерфейсов, таким образом, становится равным 2O.
static if
– простое, но мощное средство, делающее «магический форк», удваивая число вариантов использования кода. Оно позволяет писать линейный код с экспоненциальным ростом количества возможных поведений. Экспоненциального же роста генерируемого компилятором кода не происходит: вы заплатите только за те инстансы шаблонов, которые действительно используете в своём приложении.
В качестве примера использования DbI рассмотрим std.experimental.checkedint [4] – модуль стандартной библиотеки Phobos, реализующий безопасную работу с целыми числами. Какие операции с машинными целыми являются небезопасными?
Можно честно вставлять проверки после каждой операции, а можно разработать тип, который бы это делал за нас. При этом возникает множество вопросов:
Создадим «оболочку», принимающую шаблонными параметрами базовый тип и «хук», который будет осуществлять наши проверки:
static Checked(T, Hook = Abort) if (isIntegral!T) // Abort по умолчанию
{
private T payload;
Hook hook;
...
}
У хука далеко не всегда есть состояние. Давайте это учтём, использовав static if
:
struct Checked(T, Hook = Abort) if (isIntegral!T)
{
private T payload;
static if (stateSize!Hook > 0)
Hook hook;
else
alias hook = Hook;
...
}
Здесь нам на руку то, что в синтаксисе языка D точка используется и для обращения к полям объекта непосредственно, и через указатель, и к его статическим членам.
Настроим ещё и значение по умолчанию. Это может быть полезно для хуков, определяющих некоторое NaN значение. Здесь мы используем шаблон hasMember
:
struct Checked(T, Hook = Abort) if (isIntegral!T)
{
static if (hasMember!(Hook, "defaultValue"))
private T payload = Hook.defaultValue!T;
else
private T payload;
static if (stateSize!Hook > 0)
Hook hook;
else
alias hook = Hook;
...
}
В качестве примера того, как много поведений может содержать небольшой фрагмент кода, приведу перегруженные операторы инкремента и декремента.
ref Checked opUnary(string op)() return
if (op == "++" || op == "--")
{
static if (hasMember!(Hook, "hookOpUnary"))
hook.hookOpUnary!op(payload);
else
static if (hasMember!(Hook, "onOverflow"))
{
static if (op == "++")
{
if (payload == max.payload)
payload = hook.onOverflow!"++"(payload);
else
++payload;
} else
{
if (payload == min.payload)
payload = hook.onOverflow!"--"(payload);
else
--payload;
}
} else
mixin(op ~ "payload;");
return this;
}
Если хук перехватывает эти операции, делегируем их ему:
static if (hasMember!(Hook, "hookOpUnary"))
hook.hookOpUnary!op(payload);
В противном случае, обработаем переполнение:
else static if (hasMember!(Hook, "onOverflow"))
{
static if (op == "++")
{
if (payload == max.payload)
payload = hook.onOverflow!"++"(payload);
else
++payload;
} else
{
// -- аналогично
}
}
Наконец, если ничего не было перехвачено, применяем операцию как обычно:
else
mixin(op ~ "payload;");
Этот строковый миксин развернётся в ++payload;
или --payload;
в зависимости от операции.
Традиционно, отсутствие какой-то части интерфейса приводит к ошибке. Здесь же это приводит к отсутствию части возможностей:
Checked!(int, void) x; // x ведёт себя, как обычный int
В модуле std.experimental.checkedint
определено несколько стандартных хуков:
assert(0)
Хук может содержать:
А написание собственного займёт меньше 50 строчек кода. Для примера, запретим все сравнения знаковых чисел с беззнаковыми:
struct NoPeskyCmpsEver
{
static int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs)
{
static if (lhs.min < 0 && rhs.min >= 0 && lhs.max < rhs.max ||
rhs.min < 0 && lhs.min >= 0 && rhs.max < lhs.max)
{
// ассерт, сработающий во время компиляции
static assert(0, "Mixed-sign comparison of " ~ Lhs.stringof ~ " and " ~ Rhs.stringof ~ " disallowed. Cast one of the operands.");
}
}
return (lhs > rhs) - (lhs < rhs);
}
alias MyInt = Checked!(int, NoPeskyCmpsEver);
До этого Checked принимал основным параметром только базовые типы. Обеспечим композицию, позволим ему принимать другой Checked:
struct Checked(T, Hook = Abort)
if (isIntegral!T || is(T == Checked!(U, H), U, H))
{...}
Это открывает интересные возможности:
Checked!(Checked!(int, ProperCompare))
: чинить сравнения, падать в других ситуацияхChecked!(Checked!(int, ProperCompare), WithNaN)
: чинить сравнения, в других ситуациях возвращать «NaN»а также вносит бессмысленные комбинации:
и просто странные:
Для решения этого вопроса предлагается использовать «полуавтоматическую» композицию:
struct MyHook
{
alias
onBadCast = Abort.onBadCast,
onLowerBound = Saturate.onLowerBound,
onUpperBound = Saturate.onUpperBound,
onOverflow = Saturate.onOverflow,
hookOpEquals = Abort.hookOpEquals,
hookOpCmp = Abort.hookOpCmp;
}
alias MyInt = Checked!(int, MyHook);
С помощью alias
мы выбрали статические методы из существующих хуков и сделали из них свой, новый хук. Вот так мы можем расположить атомы как нам угодно!
Рассмотренный подход существует во многом благодаря static if
. Этот оператор расширяет пространство вариантов использования кода. При масштабировании Design by Introspection потребует некоторой поддержки со стороны инструментов разработчика.
Автор: Виктор
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/274904
Ссылки в тексте:
[1] странице википедии: https://en.wikipedia.org/wiki/Programming_paradigm
[2] Андрей Александреску: https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B5%D0%BA%D1%81%D0%B0%D0%BD%D0%B4%D1%80%D0%B5%D1%81%D0%BA%D1%83,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9
[3] выступления: https://www.youtube.com/watch?v=29h6jGtZD-U
[4] std.experimental.checkedint: https://github.com/dlang/phobos/blob/master/std/experimental/checkedint.d
[5] Источник: https://habrahabr.ru/post/350844/?utm_source=habrahabr&utm_medium=rss&utm_campaign=350844
Нажмите здесь для печати.