Metaprogramming with type objects in D
2016-03-01I am going to introduce you to Type Objects
and why they might be useful in combination with metaprogramming.
This blog post is heavily inspired by Boost Hana.
Let us start with an example. If we would want to implement an Algebraic Data Type
we would need to find out the maximal size of the types it should contain. In D that is pretty easy
import std.meta: AliasSeq;
import std.traits: Largest;
alias Types = AliasSeq!(int, float, char, double);
enum maxSize = Largest!(Types).sizeof;
writeln(maxSize);//8 bytes
Largest is implemented like this
template Largest(T...) if(T.length >= 1)
{
static if (T.length == 1)
{
alias Largest = T[0];
}
else static if (T.length == 2)
{
static if(T[0].sizeof >= T[1].sizeof)
{
alias Largest = T[0];
}
else
{
alias Largest = T[1];
}
}
else
{
alias Largest = Largest!(Largest!(T[0 .. $/2]), Largest!(T[$/2 .. $]));
}
}
But if Largest
wouldn't be available, it would be quite annoying to always create a new template. Alternatively we could implement Largest
like this
import std.meta: AliasSeq, staticMap;
import std.traits: Largest;
import std.algorithm.comparison: max;
alias Types = AliasSeq!(int, float, char, double);
enum size(T) = T.sizeof;
enum maxSize = max(staticMap!(size, Types));
writeln(maxSize);//8 byte
Not much worse because we can reuse the standard library but we also had to create a new template to convert types into sizes. This is a general pattern in metaprogramming. If you want to do type level metaprogramming you have to use templates but wouldn't it be nice if we could use ordinary functions?
Instead of doing type computations with templates we will create Type Objects
.
import std.traits: isInstanceOf;
enum isType(T) = isInstanceOf!(Type, T);
struct Type(T){
alias type = T;
string toString()
{
return "Type!("~T.stringof~")";
}
}
enum t = Type!(int)();
enum t1 = Type!(string)();
You may wonder why we do this. The answer is simple, we now can use Types
like ordinary objects. Let us create a simple equals
function that checks if two Types
are actually the same.
enum equals(A,B)(Type!A, Type!B){
return is(A == B);
}
enum t = Type!(int)();
enum t1 = Type!(string)();
writeln(t.equals(t1)); // false
writeln(equals(t, t1)); // false
writeln(equals(Type!int(), Type!int())); // true
Types itself are not that interesting, let us create a TypeTuple
that can hold any number of Types
.
enum isTypeTuple(T) = isInstanceOf!(TypeTuple, T);
struct TypeTuple(Types...){
import std.meta: allSatisfy;
static assert(allSatisfy!(isType, Types), "Variadic parameters need to be of type 'Type!'");
Types expand;
alias expand this;
string toString()
{
import std.range;
string[] s;
foreach(t; expand){
s~= t.toString();
}
return "TypeTuple!(" ~ s.join(", ") ~")";
}
}
enum typeTuple(Types...)(Types){
return TypeTuple!Types();
}
By the way you may wonder why we created a toString
method here. This is because D
sometimes doesn't like to print types that were generated and it might not print the actual type and inserts some pseudo symbols like F!int
instead of Type!int
.
We can use TypeTuple
like this:
enum types = TypeTuple!(Type!int, Type!char, Type!float, Type!double)();
//or
enum types = typeTuple(Type!int, Type!char, Type!float, Type!double);
It is just a bit of boilerplate which we can easily avoid with a helper function
enum tupleFromTypes(Ts...)(){
import std.meta: staticMap;
return TypeTuple!(staticMap!(Type, Ts))();
}
enum types = tupleFromTypes!(int,double, int,float);
Now we can start to implement some nice metafunctions. We start by implementing filter
.
enum filter(alias f, Tup)(Tup){
static assert(isTypeTuple!(Tup), tup.stringof~" is not a TypeTuple.");
enum tup = Tup();
static if(tup.length == 0){
return typeTuple();
}
else static if(f(tup[0])){
return typeTuple(tup[0], filter!(f)(typeTuple(tup[1..$])).expand);
}
else{
return filter!(f)(typeTuple(tup[1..$]));
}
}
We can now use it like this:
enum types = tupleFromTypes!(int, double, string, float);
enum biggerThan4 = filter!(t => t.type.sizeof > 4)(types);
writeln(biggerThan4);// TypeTuple!(Type!(double), Type!(string))
The code above filters our TypeTuple
with an ordinary lambda function. The resulting Types
must be bigger than 4 bytes. We can also implement map
enum map(alias f, Tup)(Tup){
static assert(isTypeTuple!(Tup), tup.stringof~" is not a TypeTuple.");
enum tup = Tup();
static if(tup.length == 0){
return typeTuple!();
}
else{
return typeTuple(f(tup[0]), map!(f)(typeTuple(tup[1..$])).expand);
}
}
enum types = tupleFromTypes!(int, double, string, float);
enum onlyInts = map!(t => Type!int())(types);
writeln(onlyInts);// TypeTuple!(Type!(int), Type!(int), Type!(int), Type!(int))
We replace every Type
with Type!int
which is probably not that useful. We could also replace only types that are actually bigger than 4 bytes with Type!int
.
enum types = tupleFromTypes!(int, double, string, float);
enum smallerThan5 = map!((t){
static if(t.type.sizeof > 4){
return Type!int();
}
else{
return t;
}
})(types);// TypeTuple!(Type!(int), Type!(int), Type!(int), Type!(float))
writeln(smallerThan5);
The possibilities are almost endless and you can use it like any other function. Just remember that it needs to be executed at compile time which means you have to use static if
instead of an ordinary if
.
enum indexOf(T,Tup)(T, Tup){
static assert(isTypeTuple!(Tup), tup.stringof~" is not a TypeTuple.");
static assert(isType!(T), T.stringof~" is not a Type.");
enum t = T();
enum tup = Tup();
foreach(index, type; tup.expand){
if(type.equals(t)){
return index;
}
}
return -1;
}
enum types = tupleFromTypes!(int, double, string, float);
enum index = indexOf(Type!string(), types);
writeln(index);// 2
And last but not least, we can implement quicksort for types.
enum partition(alias f, Tup)(Tup){
enum tup = Tup();
return partitionImpl!(f)(tup, typeTuple(), typeTuple());
}
enum partitionImpl(alias f, Tup, TupLeft, TupRight)(Tup, TupLeft, TupRight){
import std.typecons: tuple;
enum tup = Tup();
enum l = TupLeft();
enum r = TupRight();
static if(tup.length == 0){
return tuple(l, r);
}
else{
static if(f(tup[0])){
return partitionImpl!(f)(typeTuple(tup[1..$]), typeTuple(tup[0], l.expand), typeTuple(r.expand));
}
else{
return partitionImpl!(f)(typeTuple(tup[1..$]), typeTuple(l.expand), typeTuple(tup[0], r.expand));
}
}
}
enum sort(alias f,Tup)(Tup){
enum tup = Tup();
static if(tup.length == 0){
return typeTuple();
}
else static if(tup.length == 1){
return typeTuple(tup[0]);
}
else{
enum middle= tup[0];
enum t = partition!(t => f(t, middle))(typeTuple(tup[1..$]));
enum left = t[0];
enum right = t[1];
return typeTuple(left.expand, middle, right.expand);
}
}
I needed to create another helper function partition
which just splits a TypeTuple
into two TypeTuples
based on a predicate.
enum types = tupleFromTypes!(int, double, string, float);
enum sortedTypes = sort!((t1, t2) => t1.type.sizeof > t2.type.sizeof)(types);
writeln(sortedTypes);// TypeTuple!(Type!(string), Type!(double), Type!(int), Type!(float))
Now it is also trivial to get the maximum size
enum types = tupleFromTypes!(int, double, string, float);
enum sortedTypes = sort!((t1, t2) => t1.type.sizeof > t2.type.sizeof)(types);
enum maxSize = sortedTypes[0].sizeof;
writeln(maxSize);// 8 bytes
Type Objects
allow metaprogramming to look like normal functional programming just with types.