foreach and metaprogramming
Per Eckerdal
per.eckerdal at gmail.com
Wed Nov 8 07:27:37 PST 2006
> I'm from the speed is good camp. I like D because it seems to give good speed
> while still being easy to use.
(Sorry for the long post.) I don't think speed needs to be a problem if
you implement it as a compile-time language for lexical-aware macros.
Here is one potential way of doing this, to show you what I'm thinking
about:
This would consist of four different language features.
The first one is just syntactic sugar and should be fairly easy to
implement: Ruby-style blocks. It could be implemented as just sending a
delegate as the last function argument:
foo(1) { writefln("Hello"); }
foo(1, { writefln("Hello"); });
Would be the same. I don't know if this is hard to do, but I can't see
why it would. And it makes for some very elegant things at other places
as well, look at Ruby for examples.
The second one is two new operators, I'll call them $ and #. $ and # can
be put before expressions, statements, declarations and identifiers.
Examples are: $int a = 5; ${ /* code block */ } class $Name {}. It binds
very tightly, you should be able to assume that only the thing next to
it is bound.
# returns the AST of the following expression, $ is for declaring macro
blocks.
$ blocks may return nothing, and then they will be replaced by nothing:
${int a = 5;}; gives nothing. They can also return literals, e.g. int a
= ${return 5;}; Or, they can return AST objects, and then they will be
replaced by that tree:
${ return #{if (a == 5) die();}; } // This is equal to if (a == 5) die();
The argument to # must be a complete expression/declaration/statement,
so #{ if (a == } is invalid. The only operation you can do with ASTs
created with # is to concatenate with the ~ operator.
I think you could implement this by making all $ blocks functions in a
separate "meta"-module (used only internally when compiling). So if you
define a function within a macro block, you define a function in the
"meta"-module, essentially defining a macro that can be invoked with
${return macroname(args);}
Given this, most of the features of this can be done, but I'd suggest
adding some more syntax sugar, a macro keyword:
macro foreach;
This declares that all foreach calls should be surrounded within
${return ;} and all arguments enclosed in #{}. So you will be able to
access the compile-time function foreach as though it was a normal
function. There are one more thing to add:
macro int hello(int a) {
return a;
}
is equivalent to
${ int hello(int a) { return a; } };
macro hello;
The last feature that might be added is a library that only compile-time
code can access that allows you to reflect over classes and functions.
(Probably only in the same module as the calling code, but I don't think
that's a big limitation)
As an example I will show how you can implement a nifty kind of
iterators using this technique.
class LinkedList {
// ...
macro each(void delegate(Type t) block) {
return #{
while (iterate over the list) {
block(data);
}
};
}
// ...
}
LinkedList ll = new LinkedList;
// Add data..
// I borrowed Ruby's syntax. I don't really like it but it works as demo
ll.each { |Type data|
writefln(data);
}
(This example obviously requires the macro stuff to be done *after*
templates, but I think that makes sense.)
It can't be hard for the compiler to recognize inline delegate calls, so
this code should be able to get expanded into no method calls at all.
To sum this all up:
What I'm talking about is really only a slightly more hygienic and a lot
more powerful macro system. I can't see why this would reduce
performance, but I can see how this can be used to perform heavy
high-level optimization (for example when you have a tree that doesn't
map very well to an index operator or C++-iterators). And it would let
you do lots of other cool things as well, like automated persistence
layers without another DSL and things like that.
The great drawback I can see is that this would give much longer
compilation times. But that's the cost of doing more at compile time and
less at runtime, isn't it? (And you should be able to optimize it some,
as well)
One thing that is good about this is that you expose very few compiler
internals; only create and append AST is needed, and the rest can be
implemented as a normal (compile-time) API.
/Per
More information about the Digitalmars-d
mailing list