Everlast
| On Tuesday, 14 August 2018 at 09:12:30 UTC, ixid wrote:
> This will not compile as it says n is not known at compile time:
>
> auto fun(int n) {
> static foreach(i;0..n)
> mixin(i.to!string ~ ".writeln;");
> return;
> }
>
> enum value = 2;
>
>
> void main() {
> fun(value);
> }
>
> But making it a template parameter fun(int n)() and fun!value will obviously work as will replacing n in the foreach statement with the enum 'value'. It seems like a missed opportunity that the enum nature of 'value' does not propagate through the function call letting the compiler know that 'value'and therefore n are known at compile time.
You are not understand what is going on:
Your fun is a runtime function in n. That is, it cannot, under any circumstances, treat n as a compile time variable! n IS NOT a compile time variable... it doesn't matter how you use fun, it is irrelevant.
static foreach is a compile time loop(that is computed at compile time and the compiler can compute things about it(such as unrolling it).
CTFE means compile time FUNCTION EXECUTION!
Essentially it takes the runtime function and uses compiles it to a mini program that just contains that function, then the compiler calls the function with the *known* values. Since the function is known(defined) at compile time(obvious, since virtual all functions are in programming except self modifying functions, which are almost never used), the compiler can then evaluate the result at compile time and use that instead.
So, CTFE is no different than thinking about functions in a language without CTFE except you can treat them as also being sort of compile time functions when your inputs are known at compile time. CTFE functions will do what they behave just as if they did them at run time(although you can create different versions for compile time or run time execution if you need to have different behavior(doesn't change the conceptualization though)).
So, when I look at fun,
I see n is a run-time parameter. I then see a static foreach, which can only work over compile time(info known at compile time) using n... which states something is invalid.
The proper way:
auto fun(int n) {
[static] foreach(i;0..n)
[mixin(i.to!string ~ ".writeln;");]
i.to!string.writeln;
return;
}
and you might say! "Well, that is how I would do it at runtime!"(old school, so to speak).... YES!
CTFE is runtime! it is not compile time!
When you call
fun(3);
The compiler realizes the input, n, is actually known at compile time so it tries to do this "optimization"(which, basically, is all CTFE is)... and it will effectively compile fun and use it internally to figure out what fun(3) is.
If you want, you can think of the compiler magically rewriting fun as
auto funCTFE(int n)() {
static foreach(i;0..n)
mixin(i.to!string ~ ".writeln;");
return;
}
and then it calls not fun(3) but funCTFE(3).
or, we could see that the compiler "wraps" all functions as
auto funRT(int n) {
foreach(i;0..n)
i.to!string.writeln;
return;
}
auto funCT(int n)() {
static foreach(i;0..n)
mixin(i.to!string ~ ".writeln;");
return;
}
auto fun(int n1)(int n2)
{
if (__ctfe)
return funCT!n1;
else
return funRT(n2);
}
and then it replaces the "fun(3)" with fun!3(3). I'm not saying this is what it does internally(it could, more or less) but this is what you think about it to understand it.
When I see a function, it doesn't phase me one bit if it is CTFE or not. CTFE really has nothing to do with meta programming and it is just an optimization(pre-computes the value of something at compile time if it can so it doesn't have to be done at runtime every time it is used). It just happens that CTFE and meta programming work well together and hence they are used together often.
What you are failing at is probably making that realization(CTFE is not meta programming!! One can just use them to do really effective meta programming because it allows one to think of meta functions as normal runtime functions).
Meta programming, OTH, is realizing that if a value is defined or will be defined at compile time then you can programming against(or rather with) that to create polymorphic(different) compile time behaviors.
Templates are simply "compile time" functions(which is not the same as compile time function execution!).
Runtime function - a function that has at least one input that can only be determined when the application is ran(and not at compile time). These functions cannot be used by the compiler since the input is not known. Of course, CTFE can use them because even though the inputs are not known, from the functions perspective, they are actually known to the program and so CTFE is used to evaluate them.
Compile time function s - a function who's inputs are all known at compile time. The compiler can then evaluate these functions completely.
Now, real functions are a combination of these two categories. Old school programming only used runtime functions. The new way is to use compile time(templates, meta programming, etc) and CTFE(evaluation of runtime/compile time functions that have known inputs at compile time... which, for compile time functions, is always the case).
It's really not complex, you just have to think of the compiler working on two levels.
As it compiles a function it can figure out if it is a template function or not determine if it can "optimize"(pre-compute) the function at compile time. If it can, it uses the pre-computed result resulting in a faster program(of course, slower compilation times). If it can't, it then checks if it can use CTFE on the function(are the inputs known), and if it can, it optimizes the result(as I've shown, CTFE is effectively meta programming but one doesn't want to think about that inside the function).
If all else fails it simply resorts to using the function as if it were purely run-time and does what all compilers have done in the past.
So,
1. When you are writing a D function and you know for a fact that it will only consist of run-time behavior(you don't want to precompute stuff, or have it know anything about compile time), then you just write your function as normal.
This applies to all runtime parameters. Even if you will use them in CTFE, you don't think about that while "inside" the function. CTFE will happen automatically by the compiler when it realizes it can carry out the CTFE process.
Remember, any CTFE function is a runtime function(although, with __ctfe, it complicates things a little, but not really).
2. If you are writing a compile time function, which is a function that will only do what it does for compile time purposes, then you can use meta programming(which is programming but using functions that also work with meta programming functionality(such as traits, static stuff, etc))... which is just programming(same logic but you know the inputs are known at compile time(or should be)).
There is a different "api" though so it looks a little bit different. Lots of usage of traits and compile time introspection/reflection/etc.
3. Actual D functions are a mixture of the two concepts above. Template parameters are "compile time"(known at compile time) while function arguments are known at runtime(even if they might be known at compile time = CTFE). A D function can have both.
Once you get the hang of it, it's actually pretty simple. You just have two "modes" of thought that overlap greatly but have slight differences(they may seem vastly different but they are not). As long as you do not confuse the two modes, you will be good. One might switch between the different modes constantly and quickly.
e.g.,
ReturnType!A foo(F, alias A, int c)(int x, string s)
{
static if (is(F == string))
return F;
else
foreach(k; 0..x)
if (A(k) == 0) return s;
static foreach(i; 0..c)
writeln(s);
return "hey!";
}
So, ReturnType is a meta-programming concept. It's pretty obvious, it takes a function and returns it's return type(but it is a meta type, a "concept").
If the return type of A is an string then it gives int, if it is string, it gives string, etc. foo then has 3 template parameters and 2 runtime parameters.
The static if is a compile time if(that is done while the compiler is figuring out stuff). It checks a compile time boolean and if it is true(which, because all compile time inputs are known it can do this) it evaluates one branch, else the other.
The foreach is a runtime concept. It's inputs are assumed to only be known at runtime.
Now, add on top of that the CTFE side, which sort of turns runtime parameters in to compile time, and you hae a pretty powerful system.
The above was not necessarily showing correct or valid meta programming but that one has to think of several levels at the same time. It is not hard. One just has to keep track of all the inputs categories(are they RT or CT). Usually one the lines are clearly defined(and they actually must be for the compiler to work).
So, it's not hard, just get practicing and write some actual code. Note that to use this stuff it is very helpful to actually know what you want to achieve rather than just blinding search for things that work. e.g., suppose you wanted to write a compile time math library! The idea is that all functions can precompute their values at compile time(the inputs have to be known of course):
e.g.,
sinCT(3.42), tanCT(-42.555534), etc.
Well, guess what! You can just write a runtime library and because of CTFE, the above will work and be precomputed at compile time!
That's quite powerful! The bonus is that you have a runtime and compile time math library!
Hence, CTFE alleviates a lot of the trouble of having to versions of functions that are essentially identical. It's a very powerful optimization.
If you wanted to write, say, some type of parser that would take code at compile time and generate valid D code that then the compiler compiles, you would be doing CTFE and meta programming. D can read files at compile time and it can also create code at compile time that creates code that creates code at compile time!
One day everyone will know D! But by then it will be called something else!
|