This is an old revision of the document!


Lab 06: Design by Introspection

Over the years, a few programming paradigms have been successful enough to enter the casual vocabulary of software engineers: procedural, imperative, object-oriented, functional, generic, declarative. There's a B-list, too, that includes paradigms such as logic, constraint-oriented, and symbolic. The point is, there aren't very many of them altogether.

Design by Introspection is a proposed programming paradigm that is at the same time explosively productive and firmly removed from any of the paradigms considered canon. The tenets of Design by Introspection are:

  1. The rule of optionality: Component primitives are almost entirely opt-in. A given component is required to implement only a modicum of primitives, and all others are optional. The component is free to implement any subset of the optional primitives.
  2. The rule of introspection: A component user employs introspection on the component to implement its own functionality using the primitives offered by the component.
  3. The rule of elastic composition: A component obtained by composing several other components offers capabilities in proportion with the capabilities offered by its individual components.

static if

Let's start with a small refresher.

Templates are the feature that allows describing the code as a pattern, for the compiler to generate program code automatically. Parts of the source code may be left to the compiler to be filled in until that part is actually used in the program. Templates are very useful especially in libraries because they enable writing generic algorithms and data structures, instead of tying them to specific types.

static if is the compile time equivalent of the if statement. Just like the if statement, static if takes a logical expression and evaluates it. Unlike the if statement, static if is not about execution flow; rather, it determines whether a piece of code should be included in the program or not. The logical expression must be evaluable at compile time. If the logical expression evaluates to true, the code inside the static if gets compiled. If the condition is false, the code is not included in the program as if it has never been written.

Let's assume we want to define a use function that can use an object of type T to get, deliver and optionally wrap another object (we'll use an int for simplicity). The code for such a function will look something like the following

void use(T)(T object) {
  // ...
  int v = object.get();
  // Compute stuff on v
  object.wrap(v);
  // ...
  object.deliver();
  // ...
}

The code above uses compile-time polymorphism. Compile-time polymorphism requires that the type is compatible with how it is used by the template. As long as the code compiles, the template argument can be used with that template.

As you can see, we don't fully respect the contract, as the use of wrap is not optional. To make it optional, we'll add a static if that checks if T has a wrap method.

Q: How can we do this? A: Simple. Let's just ask the compiler if code that calls object.wrap(v) is compilable. This is easily done in the D programming language through it's traits features.

traits

Traits are extensions to the language to enable programs, at compile time, to get at information internal to the compiler. This is also known as compile time reflection. The D programming language has a comprehensive set of ready to use traits.

In our example, we are interested in the compiles trait.

Have a look at the updated code:

void use(T)(T object) {
  // ...
  int v = object.get();
  // Compute stuff on v
  static if (__traits(compiles, { object.wrap(42); }) {
    object.wrap(v);
  }
  // ...
  object.deliver();
  // ...
}

Now we respect the contract: we only call the wrap method with an int if T defines one that is callable with an int.

We can also use the is expression to achieve the same compile time check, with the following idiom:

  static if (is(typeof({ object.wrap(42); }))) {
    object.wrap(v);
  }

Though it might look complicated, it's not. What happens here is that typeof({ object.wrap(42); }) would be a compile-time error if we can't call wrap. A compile time error is not a valid type, so the result of is(error) is false. Go ahead, try the two static ifs out.

Named constraints

Now, we respect our contract, but we can do better. If a user tries to call use with a type that doesn't have get or deliver, he will get a compile time error, but the error will come from the library function instead of the user call site. To fix this, we need to add a template constraint

void use(T)(T object)
if (is (typeof(object.get())) &&
    is (typeof(object.deliver())))
{
  /* ... */
}

Although such constraints achieve the desired goal, sometimes they are too complex to be readable. Instead, it is possible to give a more descriptive name to the whole constraint:

void use(T)(T object)
if (canGetAndDeliver!T)
{
  /* ... */
}

That constraint is more readable because it is now more clear that the template is designed to work with types that can get and deliver. Such constraints are achieved by an idiom that is implemented similar to the following eponymous template:

template canGetAndDeliver(T) {
  enum canGetAndDeliver = is (typeof(
  {
    T object;
    object.get();
    object.deliver();
  }()));
}

Mixins

dss/laboratoare/06.1561978817.txt.gz ยท Last modified: 2019/07/01 14:00 by eduard.staniloiu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0