Getting components of complex types

We've seen how the is expression lets us match simple types, and how Phobos used it to extract function details previously. What about more complex types such as arrays or template instantiations? How do we break them down into details?

Getting ready

Let's define a concrete goal to accomplish. We'll make an arithmetic wrapper that should follow the same type conversion rules as built-in types, but only work with other members of the same wrapped family.

Such a type might be useful when creating a scientific units library, for example, where you want to use the type system to decorate the otherwise plain numbers with maximum flexibility. The following is an example of a complex type:

struct Number(T) if(__traits(isArithmetic, T)) {
  T value;

  this(T t) { value = t; }

  this(N)(N n) if(isSomeNumber!N) {
    this.value = cast(T) n.value;
  }
}

// convenience constructor
auto number(T)(T t) { return Number!T(t); }

template isSomeNumber(N) {
  // what's the correct implementation?
  enum isSomeNumber = !__traits(isArithmetic, N);
}

void main() {
  int b1 = 1;
  double b2 = 2.5;
  static assert(is(typeof(b1 + b2) == double));

  Number!int n1 = number(1);
  Number!double n2 = number(2.5);
  //Number!double n3 = n1 + n2; // how do we make this work?
}

There are two concrete challenges here; how can we tell if any random type is indeed an instantiation of the Number type, and how can we cause the addition operator to yield the same type we expect from the plain numbers, where double + int = double as shown by our static assert function? To do this correctly, we need to get the components of a complex type.

How to do it…

Let's execute the following steps to get components of complex types:

  1. To determine the syntax of breaking down the type, write the declaration with as many or as few placeholders as you want. If we want to accept any instantiation of the Number type, we'll write Number!T.
  2. Put a comma after it and list all the placeholders you used, optionally restraining them with : specialization or requesting for the alias symbol. So, in our case, we now have Number!T, T.
  3. Put this together in an is expression with the type we have on the left-hand side, : or == as the operator (the : operator if you want implicit conversions to be allowed, and == if you want an exact match), and the type we wrote in the previous step on the right-hand side to form the matching condition. So, we can write isSomeNumber by using is(N : Number!T, T)) to pattern-match the template instantiation, as shown in the following code:
    template isSomeNumber(N) {
      enum isSomeNumber = is(N : Number!T, T);
    }
  4. Write an opBinary method inside Number, which pattern-matches the template with the same syntax as isSomeNumber and returns the result using the convenient constructor.

The code is as follows:

  auto opBinary(string op, N: Number!T, T)(N rhs) {
    return(number(mixin("this.value " ~ op ~ " rhs.value")));
  }

After adding the preceding code, both the required tasks will be complete. The compiler does the arithmetic calculation and yields the correct type for us automatically if we give the right input.

Tip

When writing the pattern, keep your placeholders simple. If you want to match, say, an associative array of templates, first match an associative array. Then, inside that static if block, match the template separately. This will help to make your is expressions readable.

How it works…

Deconstructing complex types is done with the full form of the is expression. The full syntax of the is expression form we are using is as follows:

is(Check MatchAlias op SearchPattern, list, of, placeholders);

In the preceding code, the Check parameter is the type you are checking or deconstructing.

The MatchAlias parameter is always optional. If you choose to provide it, the type that matches the search pattern will be aliased to that name. Often, this is the same as what you passed in (the Check parameter). However, depending on the value of the SearchPattern parameter, it may extract details instead (see the previous recipe for the ReturnType implementation for an example).

The comparison operation is op. It can be either the == or : operator. The == operator checks for an exact match while the operator : checks for implicit conversions as well. You may think of the : operator as being related to inheritance, for example, class Foo : Interface will implicitly cast to Interface, so is(Foo : Interface) will be true. If you do not provide an operator, you must also not provide a SearchPattern parameter. This yields the simplest form of the is expression; it checks only if the type is valid without any specific requirements.

The SearchPattern parameter is what you're looking for. There might be a keyword such as class, function, or return if you are looking for those categories and details, or it may be a type, with as many or as few wildcard placeholders as you want to use.

Finally, you need to declare all the placeholders you used in the SearchPattern parameter in a comma-separated list. If you forget these, the compiler will issue an error about an undefined identifier. Since a template name is not a type itself, a template name must put the alias keyword before the placeholder name. Other types only need to list the name. The placeholders will be available for further inspection to find what they matched in the given Check type.

Note

You may also specialize on placeholder types with a colon followed by a type, but this can get to be difficult to read and lack the flexibility of subsequent expressions. Since they can be replaced by the nested static if statements that handle one step at a time, you may choose to rarely use this particular option.

Let's look at some examples. Previously, we used the is(N : Number!T, T) expression. Here, Check is N, the argument we passed in the template. We want to see if it implicitly converts to Number!T, which is a specific template with a placeholder parameter. If we used this expression with static if, inside that statement, we could also refer to T to see what exactly the parameter is on the type we're inspecting.

It is also instructive to look at a longer-form implementation of isSomeNumber type, which deconstructs any template, and then checks each piece for our match. This is shown in the following code:

  static if(is(N : Template!Args, alias Template, Args...))
    enum isSomeNumber = __traits(isSame, Template, Number);
  else
    enum isSomeNumber = false;
}

Here, we decomposed the entire template, which will work with any template's instantiation and not just the Number template. Then, we compared the deconstructed alias name to the original name by using __traits(isSame), which tells us whether the two names are aliases for the same symbol. If Template is an alias for Number, that's a match. We also put an else branch on the static if statement because if the checked type didn't match the pattern at all, we know it certainly isn't what we're interested in here.

The is expression is not limited to templates. Another common use is to match or deconstruct arrays and associative arrays, as shown in the following code:

template isArray(T) {
  static if(is(T == E[], E)) {
    // Note: E is the element type of the array

      enum isArray = true;
}  else
    enum isArray = false; // didn't match the E[] pattern
}
template isAssociativeArray(T) {
    static if(is(T == K[V], K, V)) {
      // Here, K and V are the key and value types, respectively
        enum isAssocativeArray = true;
    }  else
         enum isAssociativeArray = false;
}

We can get as complex as we want to match. You could write is(T == K[V][][V2], K, V, V2) if you wanted to match an associative array of arrays of associative arrays, and inspect each individual type (through K, V, and K2). However, if you specifically wanted V2, for example, to be an instance of Number!T, you should use a nested static if block to check that condition individually.

Back to our Number type, the other challenge we achieved was making opBinary work. In the opBinary compile-time argument list, we specialized out the right-hand side type using the syntax very similar to the is expression. Many pattern-matching capabilities of the is expression also work in a template argument list.

The implementation of opBinary is a bit anticlimactic. Since we wanted to match the language rules for arithmetic calculation of the constituent types, instead of trying to recreate those rules with a series of static if statements (which is possible, though tedious), we simple performed the operation and used the result. We could have also checked the result with typeof if we didn't want to use it immediately.

While compile-time reflection and static if statement give you the option of digging into the details, don't let them make you do more work than you have to. If the language itself can perform a task, let it perform that task and then simply inspect or use the result as required.

See also:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.108.175