One nice usecase for Interpolation Expression Sequences is for using mixins. In fact it was one of the primary examples in the DIP.
But one thing that always stunk about using IES for mixins is that mixin requires string data, and IES are not string data only. For instance, let’s say we want to define a function that writes the function name, as a mixin template:
mixin template FnamePrinter(string name) {
mixin(`void `, name,
`() { import std.stdio; writeln("fname is `, name, `");}`);
}
void main() {
mixin FnamePrinter!"foo";
foo(); // fname is foo
}
The benefit if interpolation sequences is to avoid some of the comma-quote spam. But because mixin takes a string only, this does not work:
mixin template FnamePrinter(string name) {
mixin(i`void $(name)() { import std.stdio; writeln("fname is $(name)");}`);
}
onlineapp.d-mixin-3(3): Error: function declaration without return type. (Note that constructors are always named `this`) onlineapp.d-mixin-3(3): Error: no identifier for declarator `InterpolationHeader()` onlineapp.d-mixin-3(3): Error: semicolon expected following function declaration, not `InterpolatedLiteral` onlineapp.d-mixin-3(3): Error: function declaration without return type. (Note that constructors are always named `this`) onlineapp.d-mixin-3(3): Error: no identifier for declarator `InterpolatedLiteral()` ...
In order to pass it, you need to convert it to a string or a sequence of strings. The current “accepted” way to do this is to use std.conv.text
which will convert an IES to a string:
mixin template FnamePrinter(string name) {
import std.conv;
mixin(i`void $(name)() { import std.stdio; writeln("fname is $(name)");}`
.text);
}
There is a cost to this however. In this case, the text
function is running at compile time — building strings, calling all kinds of functions from phobos, and generally will explode your compile time and memory usage.
However, what we really want is the same thing as the hand written mixin. And IES are tantalizingly close. Let’s see what the given sequence actually is. To do this, we will print the type of the expression.
pragma(msg,
typeof(i`void $(name)() { import std.stdio; writeln("fname is $(name)");}`));
(InterpolationHeader, InterpolatedLiteral!"void ", InterpolatedExpression!"name", string, InterpolatedLiteral!"() { import std.stdio; writeln(\"fname is ", InterpolatedExpression!"name", string, InterpolatedLiteral!"\");}", InterpolationFooter)
This is what a standard IES tuple is. You always have the header and footer to help delineate where the IES expressions start and end. Those shouldn’t be included for mixing in, as they are just markers.
Then you have InterpolatedLiteral
to denote the actual literal portions of the text. Each literal type has a toString
function which provides the string template parameter. then you have InterpolatedExpression
which represents the string that was used inside the interpolation to form the expression (in this case, it’s always "name"
). These, we don’t need to include, because that’s the local name.
And finally, we get the string
that is the actual name we want to include. What we need is a new sequence that only contains the mixin-able parts (the strings). We can remove all the rest, and make sure to convert anything that’s a literal into it’s corresponding string.
To do that, we use a template, with a reassignable alias sequence (a relatively recent feature added to D):
import std.meta : AliasSeq;
import core.interpolation;
enum shouldRemove(T) =
is(T == InterpolatedExpression!str, string str) ||
is(T == InterpolationHeader) ||
is(T == InterpolationFooter);
enum isILit(T) = is(T == InterpolatedLiteral!str, string str);
template Code(Args...)
{
alias result = AliasSeq!();
static foreach(i; 0 .. Args.length)
static if(!shouldRemove!(typeof(Args[i])))
{
static if(isILit!(typeof(Args[i])))
result = AliasSeq!(result, Args[i].toString());
else
result = AliasSeq!(result, Args[i]);
}
alias Code = result;
}
Let’s go through the pieces of this.
First, we have to import the interpolation types so we can use them, along with the ubiquitous AliasSeq
. The Code
template’s goal is to take an IES, and remove all unneeded parts, and convert all used parts into strings when necessary.
There is a reason I put the two checking templates outside the other template, I’ll get to that later. But these helper templates make it so we can write more succinct code in the loop. The shouldRemove
template checks to see if the type should be removed from the mixin sequence. Headers, footers, and expressions don’t need to be in there, so this matches any of those.
The static foreach
will give us an unrolled loop with i
being the index of each item we are looking at. We check if we shouldn’t remove it, and if so, we check to see if it’s a literal. Because the literal isn’t automatically converted to a string, this is the one place where we have to fire up the CTFE interpreter to call the toString
function. Otherwise, we just pass the expressions as-is.
The end result looks very similar to the .text
call, but invokes almost no CTFE:
mixin template FnamePrinter(string name) {
mixin(Code!i`void $(name)() { import std.stdio; writeln("fname is $(name)");}`);
}
The only ugly-ish part is we have to pass the IES as a template parameter, and so we can’t use UFCS to call it.
However, the cost here is tremendously less. If you want to see what importing std.conv and then calling text
includes, just fire up the compiler and print out the AST generated (-vcg-ast
switch). In this case, we include three templates, and rely on the compiler to do any string conversions necessary.
Just to prove we are doing what we want, let’s print the converted IES sequence:
pragma(msg,
Code!i`void $(name)() { import std.stdio; writeln("fname is $(name)");}`);
AliasSeq!("void ", "foo", "() { import std.stdio; writeln(\"fname is ", "foo", "\");}")
Exactly as if we did it by hand!
So what about that note about not including the templates in the loop? I originally tried this, but the compiler complains, because the statement:
static if(is(typeof(Args[i]) == InterpolatedExpression!str, string str))
Introduces a new symbol named str
, even though I don’t use it. But this is how pattern matching with is
expressions works, and there’s no way around it. When the loop continues and a second str
is defined, the compiler complains that str
is being redefined.
Without a way to declare str
as a static foreach local (a proposed feature of static foreach that was not accepted), we can’t use it in the loop. This awkwardness is something I wasn’t planning on discussing, but I thought it would be interesting to readers and also show that D still has some rough edges when it comes to metaprogramming.
“But this is how pattern matching with is expressions works, and there’s no way around it.”
This just works in OpenD. It wasn’t even a large patch! Local vars in static foreach was never accepted at the language level upstream, but it *was* implemented inside the compiler, so we simply set that flag for static if(is()) conditions. Seems to just work with no negative side effects; the only code whose behavior it changes was a compile error before.
See for yourself:
https://github.com/opendlang/opend/commit/09ef2a4fdcda3f33d18115460c45d639beee97c4
(a slight extension of https://github.com/opendlang/opend/commit/7748bad7c08f3587cab4626c7b8fff9019ab1f15 cuz we missed a spot the first time)
so with this, your code simplifies to:
“`
import std.meta : AliasSeq;
import core.interpolation;
template Code(Args…)
{
alias result = AliasSeq!();
static foreach(Arg; Args)
static if(
is(typeof(Arg) == InterpolatedExpression!str, string str)
||
is(typeof(Arg) == InterpolationHeader) || is(typeof(Arg) == InterpolationFooter)
)
result = result; // skip ’em
else static if(is(typeof(Arg) == InterpolatedLiteral!str, string str))
result = AliasSeq!(result, Arg.toString());
else
result = AliasSeq!(result, Arg);
alias Code = result;
}
enum name = “main”;
pragma(msg,
Code!i`void $(name)() { import std.stdio; writeln(“fname is $(name)”);}`);
“`
This retains a single template instance in memory but doesn’t output it to the object file. About as good as these tricks get, i think.