On Monday, 21 June 2021 at 20:01:27 UTC, Steven Schveighoffer wrote:
>I have some code in my codebase that does stuff like this:
struct S
{
int id;
int field1;
}
auto foo(S[] arr, int someval) // @nogc not allowed :(
{
// remove all the elements of arr that have someval for field1
import std.algorithm;
return arr.filter!(v => v.field1 == someval);
}
While this works, and is nice, it's using a lambda with local references to filter data. This means a GC closure allocation is involved.
However, the value that is referenced (someval) is the only thing needed, and it seems a huge waste to allocate a stack frame just for that.
Let's look at another way to do this. filter
does not have an overload which accepts a value to compare to, but find
does. So let's build a new filter
that uses it:
auto nogcfilter(alias pred, T, V)(T[] rng, V val)
{
import std.range;
import std.algorithm : find;
static struct Filter {
T[] src;
V val;
auto front() { return src.front; }
void popFront() {src.popFront; src = src.find!pred(val); }
auto empty() { return src.empty; }
}
return Filter(rng, val);
}
auto foo(S[] arr, int someval) @nogc // :)
{
// note: the explicit lambda types are needed for some reason
return arr.nogcfilter!((S v, int a) => v.field1 == a)(someval);
}
Now we get filter capability, but without the GC.
This works great because the context items used do NOT require any mutability. It also works great because the filter function is building a custom type ANYWAY.
Now, we could add this kind of functionality to filter
, but being the lazy programmer that I am, what if the compiler could do this for me? In most cases, I don't want to sleuth out what context is needed (which might change as the code evolves). To force the user to both invent the context type (here, it's just an int
, but it may require other values) and explicitly pass it is a bit verbose (I already don't like having to pass the int
, and declaring the parameter).
What if, a function that accepts a lambda, could also accept a context which the compiler made up and passed in? Here's how it would possibly look (with a new magic type __context
):
auto nogcfilter(alias pred, T, __context...)(T[] rng, __context ctxt)
{
import std.range;
import std.algorithm : find;
static struct Filter {
T[] src;
__context val;
auto front() { return src.front; }
void popFront() {src.popFront; src = src.find!pred(val); }
auto empty() { return src.empty; }
}
return Filter(rng, ctxt);
}
auto foo(S[] arr, int someval) @nogc
{
return arr.nogcfilter!((v) => v.field1 == someval);
}
The compiler would see there's a lambda involved, automatically pass the context parameter someval
into the function itself, and everything magically works. It would only be used when a closure would otherwise be allocated, and the function being called declares the context parameter.
Disclaimer: I am not a language developer.
Does this make sense? Is there anything feasible that might be similar? Would it be more feasible with different syntax?
I realize there are implications if the context parameter is mutable, so perhaps we would have to require only const contexts (though the compiler might be able to figure out that something isn't modified anywhere, and allow it to go in).
-Steve
Why can't it be implemented by using templates?
- Alex