-
-
Notifications
You must be signed in to change notification settings - Fork 706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement naryFun #3882
Implement naryFun #3882
Conversation
| If $(D fun) is not a string, $(D naryFun) aliases itself away to $(D fun). | ||
| */ | ||
|
|
||
| template naryFun(size_t argc, alias fun, parmNames...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is argc really necessary? Looks like parmNames.length.
edit:
I see that it has a path for default parameter names, but this can be added as an overload.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless paramNames isn't given. Then we need argc. I guess I could use an overload instead.
On December 20, 2015 8:50:35 PM EST, JakobOvrum notifications@github.com wrote:
@@ -260,6 +210,83 @@ unittest
static assert(!is(typeof(binaryFun!FuncObj)));
}+/**
+Transforms a string representing an expression into a n-ary
function.
+The number of arguments is given as $(argc).
+The string must either use symbol names $(D a-z) as the parameters
or
+provide the symbols via the $(D parmNames) arguments.
+If$(D fun) is not a string, $ (D naryFun) aliases itself away to $(D
fun).
+*/
+
+template naryFun(size_t argc, alias fun, parmNames...)Is
argcreally necessary? Looks likeparmNames.length.
Reply to this email directly or view it on GitHub:
https://github.com/D-Programming-Language/phobos/pull/3882/files#r48112338
|
While it does seem like 26 parameters ought to be enough for anybody , this arbitrary limitation is not necessary. Fix: /**
Transforms a string representing an expression into a n-ary function.
$(UL
$(LI The number of parameters is given by $(D argc). If no explicit
$(D argc) is given, it is inferred to be $(D parmNames.length).)
$(LI Explicit names for the function parameters may instead be given as
$(D parmNames). If more than $(D argc) names are supplied, the
extras will be ignored.)
$(LI If no $(D parmNames) are given, the letters ($(D a) through $(D z))
are used to name up to the first 26 function parameters.)
$(LI $(D args[index]) can always be used to access any parameter by index,
regardless of whether it has been given a name.)
)
*/
template naryFun(alias fun, size_t argc, parmNames...)
if (parmNames.length == 0 || allSatisfy!(isSomeString, typeof(parmNames)))
{
static if (is(typeof(fun) : string))
{
// No need to regenerate the aliases mixin for every ElementTypes permutation.
private enum aliases = naryFunAliases(argc, parmNames);
static if (!fun._ctfeMatchNary(parmNames))
{
import std.traits, std.typecons, std.typetuple;
import std.algorithm, std.conv, std.exception, std.math, std.range, std.string;
}
auto naryFun(ElementTypes...)(auto ref ElementTypes args)
if (ElementTypes.length == argc)
{
mixin(aliases);
return mixin(fun);
}
}
else static if (needOpCallAlias!fun)
{
// Issue 9906
alias naryFun = fun.opCall;
}
else
{
alias naryFun = fun;
}
}
/// ditto
template naryFun(alias fun, parmNames...)
if (allSatisfy!(isSomeString, typeof(parmNames)))
{
alias naryFun = naryFun!(fun, parmNames.length, parmNames);
}
// Generate naryFun parameter aliases mixin string.
private string naryFunAliases(size_t argc, string[] parmNames...) pure
{
import std.algorithm : min;
import std.conv : to;
if(parmNames.length > 0)
{
string ret;
foreach(i, name; parmNames[0 .. min(argc, parmNames.length)])
ret ~= "alias " ~ name ~ " = args[" ~ i.to!string ~ "];";
return ret;
}
else
{
// The default a-z aliases are always the same, so cache them at global scope.
enum size_t az = ('z' - 'a') + 1;
enum all = function()
{
string ret;
foreach(i; 0 .. az)
ret ~= "alias " ~ cast(char)('a' + i) ~ " = args[" ~ i.to!string ~ "];";
return ret;
}();
enum ends = function()
{
size_t[az + 1] ret;
size_t i = 1, j = 0;
while(i <= az)
{
while(all[j++] != ';') { }
ret[i++] = j;
}
return ret;
}();
// Uses slices to minimize CT allocations.
return all[0 .. ends[argc > az? az : argc]];
}
}Three other problems solved by the above are:
EDIT: Updated to cache the default |
|
@rcorre I have updated my previous comment; please read the latest version on Github rather than the original email notification. |
|
@tsbockman I don't think I need to re-add Interesting point about re-evaluating |
|
Never mind, I see why you did it like that now. |
I do want to support that, as a generalized solution to the Will anyone ever use this feature? Who knows! It costs nothing to expose |
Some of the generic code I've been working on recently accepts hundreds of different template argument permutations. Compilation of my exhaustive tests is annoyingly slow - as in C++ slow. The compiler's memory use is kind of disturbing at times, as well. This has got me into the habit of considering the compile-time costs of my CTFE and template use. It takes some getting used to, but ultimately the only really big differences compared to runtime optimization, are that the CTFE environment is about 100x slower, and leaks like a sieve. |
|
@tsbockman I basically took your suggestion line-for-line. Counting semicolons in the
My (naive) mindset so far has been 'if it doesn't happen at runtime, I can ignore performance' :) |
Yes that was a bit of a hack on my part. I thought about cobbling together some things from
If the program never finishes compiling, then nothing happens at runtime. Best performance possible! 😉 |
The problem with that, as I found out in my earlier attempts, is that this code path is hit by so many other functions. I would have preferred |
I hadn't thought that far ahead, but it makes sense that circular dependencies would be an issue here.
Fixed now: // Generate naryFun parameter aliases mixin string.
string naryFunAliases(size_t argc, string[] parmNames...) pure
{
import std.array : appender;
import std.algorithm : min;
import std.conv : to;
if(parmNames.length > 0)
{
auto ret = appender!string();
foreach(i, name; parmNames[0 .. min(argc, parmNames.length)])
ret ~= "alias " ~ name ~ " = args[" ~ i.to!string ~ "];";
return ret.data;
}
else
{
// The default a-z aliases are always the same, so cache them at global scope.
enum size_t az = ('z' - 'a') + 1;
enum cache = function()
{
static struct R
{
string all;
size_t[az + 1] ends;
}
R ret;
auto buff = appender!string();
foreach(i; 0 .. az)
{
buff ~= "alias " ~ cast(char)('a' + i) ~ " = args[" ~ i.to!string ~ "];";
ret.ends[i + 1] = buff.data.length;
}
ret.all = buff.data;
return ret;
}();
// Uses slices to minimize CT allocations.
return cache.all[0 .. cache.ends[argc > az? az : argc]];
}
} |
|
Very clever! I've integrated that now. |
| } | ||
|
|
||
| // Generate naryFun parameter aliases mixin string. | ||
| string naryFunAliases(size_t argc, string[] parmNames...) pure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I somehow dropped the private access modifier here, by accident. Please put it back.
|
Re-added |
| alias unaryFun = fun; | ||
| } | ||
| } | ||
| alias unaryFun(alias fun, string parmName = "a") = naryFun!(fun, parmName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To take full advantage of the CT optimizations to naryFun, this should now be:
template unaryFun(alias fun, parmName...)
if(parmName.length <= 1)
{
alias unaryFun = naryFun!(fun, 1, parmName);
}
template binaryFun(alias fun, parmNames...)
if(parmNames.length <= 2)
{
alias binaryFun = naryFun!(fun, 2, parmNames);
}Otherwise unaryFun and binaryFun - by far the most common instantiations of naryFun for the foreseeable future - won't benefit from the caching of the default a through z parameter names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do slightly prefer the original signatures. unaryFun(alias fun, parmName = "a") is more clear than unaryFun(alias fun, parmName...) if (parmName.length <= 1).
If we turn naryFunAliases into a template, then multiple instantiations with the same argc won't have to re-evaluate. The problem is that cache would be re-evaluated for each different argc. You can un-nest it from naryFunAliases to avoid this, but I'd rather not create too many top-level private templates just to support this one. I thought maybe static enum would do the trick, but it doesn't look like that works.
I'll just use the changed signatures for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's what it looks like as a template with external enumsand nested enums. The pragma is triggered each time for the latter case.
It's a little repetitive, but perhaps this is more to your liking? alias unaryFun(alias fun) = naryFun!(fun, 1);
alias unaryFun(alias fun, string parmName) = naryFun!(fun, 1, parmName);
alias binaryFun(alias fun) = naryFun!(fun, 2);
alias binaryFun(alias fun, string parm1Name) = naryFun!(fun, 2, parm1Name);
alias binaryFun(alias fun, string parm1Name, string parm2Name) = naryFun!(fun, 2, parm1Name, parm2Name);The point is really just to avoid the |
|
On lines 278 and 300, I should have written The However, this may be moot, as from my testing it looks suspiciously like a copy always occurs if the slicing is performed at compile time. Does DMD not do compile-time string interning (de-duplication)? This seems like low-hanging fruit to cut down on executable size and working set a bit. |
|
Anyway, we're splitting hairs at this point; just pick any of the three different fixes we discussed for the After that, I'd say this is ready to pull. |
|
Rebased, squashed, and ready to go. I went with the first option you suggested. Thanks for all the insight! |
LGTM. 👍 @JakobOvrum ? |
| @@ -104,30 +104,10 @@ the parameter or provide the symbol via the $(D parmName) argument. | |||
| If $(D fun) is not a string, $(D unaryFun) aliases itself away to $(D fun). | |||
| */ | |||
|
|
|||
| template unaryFun(alias fun, string parmName = "a") | |||
| template unaryFun(alias fun, parmName...) | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
template unaryFun(alias fun, string paramName)|
@JakobOvrum That would change the semantics, as An alternative we considered earlier would be this: alias unaryFun(alias fun) = naryFun!(fun, 1);
alias unaryFun(alias fun, string parmName) = naryFun!(fun, 1, parmName);
alias binaryFun(alias fun) = naryFun!(fun, 2);
alias binaryFun(alias fun, string parmName) = naryFun!(fun, 2, parm1Name);
alias binaryFun(alias fun, string parmName0, string parmName1) = naryFun!(fun, 2, parmName0, parmName1);Is that acceptable? |
|
How about the old signatures using default arguments? |
|
@JakobOvrum Specifying the default arguments in that way defeats the caching of the Given this is exactly the sort of template which is likely to be deeply nested inside of others, I'd like it to minimize its compile-time costs. |
| @@ -42,8 +42,8 @@ $(TR $(TH Function Name) $(TH Description) | |||
| $(TR $(TD $(D $(LREF toDelegate))) | |||
| $(TD Converts a callable to a delegate. | |||
| )) | |||
| $(TR $(TD $(D $(LREF unaryFun)), $(D $(LREF binaryFun))) | |||
| $(TD Create a unary or binary function from a string. Most often | |||
| $(TR $(TD $(D $(LREF unaryFun)), $(D $(LREF binaryFun)), $(D $(LREF naryFun))) | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better use the backticks in new code instead of $(D ...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be $(LREF unaryFun) or just unaryFun?
|
@JakobOvrum it is possible to keep the original signature and leverage the cached strings by making |
|
How about the commit I just added? Making |
Oh I remembered it wrongly to be in the first byte. Yep that's sufficient for me :) |
|
Ping @JakobOvrum @9il @andralex . Can we move forward with this? It's blocking another PR by @rcorre . |
|
Any updates here? Waiting on this to improve #3837. |
|
I've hinted at this before, but I think the |
|
@JakobOvrum I'd say no. It was imitating the use of default names for |
|
@JakobOvrum : see those two commits for what it looks like without This does miss the case where someone wants a 50-param function and doesn't want to pass in 50 param names (they just want to use Your |
stringLambda translates a string lambda into a function. The function can have an arbitrary number of args. These are assumed to use the letters a-z, but the caller can provide custom arg names. If the string lambda uses > 26 args, the caller can use args[n] to access the nth arg. This is being implemented to support multi-arg std.algorithm.each. Also replaces $(D ...) with `...` in stringLambda docs. Includes a number of ideas from @tsbockman to: - reduce CTFE load - enforce matching argument count - keep template internals private naryFunAliases is implemented as a private template _outside_ of to reduce CTFE work by caching the result when fun differs but paramNames are the same. This ensures the aliases are computed only once for each unique set of paramNames or each value of argc.
unaryFun and binaryFun are now just shortcuts for naryFun. Implement them as aliases to remove duplicate code. This removes the need for _ctfeMatchUnary, and _ctfeMatchBinary as well.
Based on @9il 's suggestions, this rework is a bit complex, but: * It doesn't depend on anything else in Phobos. * It compiles about 40% faster in my tests. * It will hardly ever allocate more than twice. It also comes with some unit tests.
Implement a couple more small suggestions from @9il. Remove obsolete test.
Replaced O(N^2) _ctfeMatchNary() with O(N log(N)) strLambdaNeedsImports(). Renamed stringLambdaAliases to strLambdaAliases for brevity.
The separation between argc and paramNames was needlessly complex. Clean up the implementation/interface/documentation by requiring the caller to provide a param name for every arg. This also makes the usage more clear. `stringLambda!(fun, 2)` is less clear than `stringLambda!(fun, "a", "b")` `stringLambda!(fun, 3, "x", "y")` is _much_ less clear than `stringLambda!(fun, "x", "y", "c")`. If a user has a need for many params, they can generate a list of names to pass into stringLambda (which should now be easy with `aliasSeqOf`).
This example shows how aliasSeqOf can be used to generate names when using stringLambda with a many-parameter function. The example is a bit convoluted as the chars of std.ascii.lowercase must be mapped to individual strings. An alternative is to allow paramNames to take chars as well as strings, but this would complicate the implementation.
This PR is complete overengineering and I would like to close it with related PRs. |
|
I'm ok with that. I don't feel particularly strongly about this and have other PRs I'd rather focus on. Any other opinions on closing this and possibly #3837? |
@9il, please, this is not a productive comment. The motivations here have been clearly stated. What are the authors supposed to do when presented with a comment like this? Please don't close pull requests (or threaten to) simply because you don't like them. If you think something is a bad idea, please elaborate on why - there's a chance your comment will guide the work in a good direction. @rcorre, I think the current proposal makes a lot of sense, but I hope to hear from @9il about his considerations. |
I contributed to this PR too, because I was afraid that this PR can be merged into Phobos and I wanted to reduce issues of this PR. So, I tried to improve it. Later, when I was included to Phobos core team I decide to do not spend time on it. This PR wanted to be improvement of my import workaround for string lambdas. I will not describe why I don't like it to be merged because this is about style/time/architecture and not about algorithms and clear reasons. This is just my opinion. |
While there are probably other legitimate criticisms that could be made of my work on That's a term usually used to attack excessively complex solutions, but
|
|
That's true, I think there was a fair bit of duplicate code between @tsbockman could you double-check me on that last commit? I think the buffer length guess was off by 1. |
That commit is not helping anything. The buffer was already large enough for typical usage, and in the rare case that someone is using long variable names, this bit will grow it as needed: while(ret_buff.length < (ret.length + line + name.length + i.length))
ret_buff.length *= 2;What problem are you trying to solve? |
The initial size of that buffer has no significance, other than as a performance optimization. The code should still work as long as the initial size is greater than zero and not too ridiculously large. |
|
@rcorre Looking at this again (it's been a while since I wrote this stuff)... You should undo that change; it messes up the (formerly) perfect length calculation in the auto ret_buff = new char[paramNames.length * (line + 8)];In particular, the |
|
I assumed it was guessing the length of a single variable name as an optimization, and it looked wrong based on the string it was generating. I didn't read carefully enough. Undoing it now. |
I do agree with @tsbockman and @rcorre that this is pretty useful. |
|
So this adds a considerable amount of sophistication to an approach that is already being obsoleted. There's also a sort of decline in usefulness - string lambdas are best when the body is very short, but the more arguments functions have the more bulky body they also tend to have. I might be missing something, e.g. uses of the imports analysis or other ancillary functions. But if the sole purpose of this is to generalize I'll close this; feel free to reopen or redo if it seems I'm mistaken. |
naryFunis likeunaryFun/binaryFun, but for an arbitrary number of args.Motivated by #3837