What if we were able to arrange the atoms one by one as we like?
Richard Feynman
How many programming paradigms can you name? The list on this Wikipedia page contains as many as 76 titles. This list can be supplemented by another approach called Design by Introspection. Its main idea is to actively use simple tools for metaprogramming and introspection of types (compile time) to create elastic components.
The author of this approach is Andrei Alexandrescu . The article used materials from his speech at Dons 2017.
In 2001, a pattern called policy-based design was introduced in the book Modern Design in C ++. In general, this is a “strategy” pattern, but using patterns and compiled at compile time. The host host class takes with its parameters a set of policy types that implement each some kind of independent functionality, and internally uses the interfaces provided by these components:
struct Widget(T, Prod, Error) { private T frob; private Prod producer; private Error errPolicy; void doWork() { // // duck-typing } }
Here the pattern is described by a short syntax. (T, Prod, Error)
- its parameters.
Instantiation looks like this:
Widget!(int, SomeProducer, SomeErrorPolicy) w;
The advantages are obvious: the effectiveness of templates, good separation and reuse of code. However, the components are integral, non-separable. If any part of the interface is missing, this will lead to a compilation error. Let's try to develop this scheme, to give the components "plasticity".
So, for this we need:
Let's take a look, which means of the D language can be used for each of the points:
.tupleof
, __traits
, std.traits
__traits
is a reflection tool built into the compiler. std.traits
is a library extension of embedded traits, among which we will be interested in the hasMember
function.static if
, static foreach
static if
, static foreach
are if
and foreach
compile times.The most important feature of Design by Introspection is the optional interfaces. Here the component contains R mandatory primitives (maybe 0) and O optional. With the help of introspection, you can find out whether a particular primitive is defined, and knowledge of the missing primitives is as important as the ones that the component contains. The number of possible interfaces thus becomes 2 O.
static if
is a simple but powerful tool that makes the "magic fork", doubling the number of code use cases. It allows you to write linear code with an exponential increase in the number of possible behaviors. Exponential growth of the code generated by the compiler does not occur: you pay only for those instances of templates that you really use in your application.
As an example of using DbI, consider std.experimental.checkedint , a module of the Phobos standard library that implements safe operation with integers. What operations with machine integers are unsafe?
You can honestly insert checks after each operation, and you can develop a type that would do it for us. This raises many questions:
Create a “shell” that accepts base type with template parameters and “hook”, which will perform our checks:
static Checked(T, Hook = Abort) if (isIntegral!T) // Abort { private T payload; Hook hook; ... }
A hook does not always have a condition. Let's take this into account using static if
:
struct Checked(T, Hook = Abort) if (isIntegral!T) { private T payload; static if (stateSize!Hook > 0) Hook hook; else alias hook = Hook; ... }
Here we are in favor of the fact that in the syntax of the D language, the dot is used to refer to the fields of the object directly, and through the pointer, and to its static members.
Adjust the default value as well. This can be useful for hooks that define some NaN value. Here we use the hasMember
template:
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; ... }
As an example of how many behaviors may contain a small piece of code, I’ll give you overloaded increment and decrement operators.
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; }
If the hook intercepts these operations, delegate them to him:
static if (hasMember!(Hook, "hookOpUnary")) hook.hookOpUnary!op(payload);
Otherwise, handle the overflow:
else static if (hasMember!(Hook, "onOverflow")) { static if (op == "++") { if (payload == max.payload) payload = hook.onOverflow!"++"(payload); else ++payload; } else { // -- } }
Finally, if nothing is intercepted, apply the operation as usual:
else mixin(op ~ "payload;");
This string mixin will unfold in ++payload;
or --payload;
depending on the operation.
Traditionally, the absence of some part of the interface leads to an error. Here, too, this leads to the absence of a part of the possibilities:
Checked!(int, void) x; // x , int
The std.experimental.checkedint
module defines several standard hooks:
assert(0)
A hook can contain:
And writing your own will take less than 50 lines of code. For example, we prohibit all comparisons of signed numbers with unsigned ones:
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);
Prior to this, Checked accepted only basic types as the main parameter. Provide a composition, let it take another Checked:
struct Checked(T, Hook = Abort) if (isIntegral!T || is(T == Checked!(U, H), U, H)) {...}
This opens up interesting possibilities:
Checked!(Checked!(int, ProperCompare))
: fix comparisons, fall in other situationsChecked!(Checked!(int, ProperCompare), WithNaN)
: repair comparisons, in other situations return “NaN”as well as making meaningless combinations:
and just weird:
To resolve this issue, it is proposed to use a “semi-automatic” composition:
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);
With the help of alias
we selected static methods from existing hooks and made of them our own new hook. This is how we can arrange atoms as we like!
The considered approach is largely due to static if
. This operator expands the code use space. When scaling Design by Introspection will require some support from the developer tools.
Source: https://habr.com/ru/post/350844/
All Articles