📜 ⬆️ ⬇️

Design by Introspection

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.


Prehistory


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 } } 

Syntax Explanation

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".


Requirements


So, for this we need:


  1. Introspection of types: “What are your methods?”, “Do you support the xyz method?”
  2. code execution at compile time
  3. code generation

Let's take a look, which means of the D language can be used for each of the points:


  1. .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.
  2. CTFE , static if , static foreach
    At compile time, you can perform a large class of functions (in fact, any functions that are portable and have no global side effects).
    static if , static foreach are if and foreach compile times.
  3. patterns and mixins
    Mixins in the D language are of two types: template and string. The former serve to insert a set of definitions (functions, classes, etc.) in some place of the program. The second ones turn the string formed at compile time directly into the code. String mixins are usually used in small portions.

Optional Interfaces


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.


Example


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.


Function code entirely
 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:



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); 

Composition


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:



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!


Conclusion


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