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
Make Ranges Chainable through Concatenation Operator ~ #3353
Conversation
@@ -1106,6 +1106,16 @@ if (Ranges.length > 0 && | |||
} | |||
return result; | |||
} | |||
|
|||
auto opCat(Range)(Range r) |
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.
opCat()
is D1, please us opBinary(string op : "~")() instead.
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.
Done.
I'm against this change. I think regardless of the noise, there are some safety issues with chaining that are left unsolved. There is also the problem of using arrays as ranges, as This is akin to me to having I realize I might be pissing in the wind here, as Walter made it a part of his presentation at dconf2015 to have this done :) |
@@ -1106,6 +1106,16 @@ if (Ranges.length > 0 && | |||
} | |||
return result; | |||
} | |||
|
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.
Why is this not doing the mixin?
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.
Because I needed a specialized logic for flattening of the chain
-tree as mentioned below.
If this flattening is not needed we can reuse Chainable
here, of course.
If not, I guess it's better to move this tree-flattening logic to Chainable
...aah but because Result is a Voldemort type I don't think this is possible.
My plan now instead is to do something like
auto opBinary(string op : "~", Range)(Range r)
if (isInputRange!(Unqual!Range))
{
import std.range : chain;
static if (isInstanceOf(Range, Result)) // if rhs r is a chain
{
return chain(source, r.source); // flatten chain-tree
}
else
{
return chain(source, r); // flatten chain-tree
}
}
Is there some way to check if rhs argument r is a chain?
What do you say?
Is this in vain?
I think the usage of template mixins is clever.
That's a good point I had not considered. You're right that |
@WalterBright Isn't Four ranges instead of five, in this case. Should I remove this flattening of the |
@schveiguy Can you give a concrete code example of the problem with the stack allocation you mentioned above? |
Sure: import std.range;
void main()
{
uint[4] arr1, arr2;
auto chain1 = arr1[] ~ take(arr2[], 2); // allocates
auto chain2 = arr1[] ~ retro(arr2[0..2]); // doesn't allocate?
} The fact that chain1 has infinite lifetime and chain2 has (should have) scoped lifetime is not apparent. The subtle difference makes chain2 less safe, which means either we have to disallow more things with it (inexplicably to the user), or we must put warning signs everywhere. These kinds of bugs easily creep in as code evolves, or with genericity that doesn't foresee what types of possible args will be passed in. However, it's quite obvious that when one explicitly calls |
What about only defining opCat for ranges that are of the same type? Then the problem you describe disappears. |
Imagine a function that at one point chains two ranges together. Each satisfies isInputRange. Who is going to "specialize" on two homogenous range types in order to call |
@schveiguy What do you think about @MetaLang 's latest comment? |
I may not have been specific, but my latest comment was in reply to that. |
@schveiguy, can you please give a code example of your comment containing: Imagine a function that at one point chains two ranges together. Each satisfies that shows what is possible with |
@schveiguy, in what way is
{
uint[4] arr1, arr2;
auto chain1 = arr1[] ~ take(arr2[], 2); // allocates
auto chain2 = arr1[] ~ retro(arr2[0..2]); // doesn't allocate?
} ? When returned from a function? Code example, please. |
The best we could do is disallow the line for creating I'm also finding that I look at things these days more with an eye towards "how can this code change over time", and I can easily see someone replacing an array with a non-array range (or vice versa), changing the meaning of
I could, but don't really want to :) It's tedious to invent such an example. But my point simply is, nobody is going to take advantage of |
IMHO, the root of the problem here is that Should/could this be disallowed to compile, @WalterBright @andralex ? If this could be made to be disallowed would operator overloading still be a good idea if we gave DMD user diagnostics some love, @schveiguy ? Is this somehow related to DIP-25, @WalterBright ? What about extending DMD with some static analysis to figure out if a data structure recursively contains any references to stack allocated data, @WalterBright ? Is there perhaps already some DMD logic that detects this, @WalterBright ? |
FWIW, under my proposal the compiler would correctly detect that it refers to stack data and would prevent returning it (or for that matter, would already disallow construction of the range). |
What do you mean with your proposal? |
I don't think so. What happens is that you have a seemingly arbitrary decision as to whether |
|
@schveiguy, why is this an issue now that we have @nogc? |
It's an issue with what But really, the issue is the silent changing of behavior. When I use one range type, it does one thing, but another range type does another thing. If I change one range type (let's say I really wanted the retro of an array), then all of a sudden the behavior changes. With On top of that, it's up to the implementer of a range to continue this trend. If I write |
@schveiguy, thanks for you thorough answer. Now I understand your standpoint. I guess we'll have to see what the other people think about this. For the record: AFAIK, import std.range: chain;
int[3] x = [1, 2, 3];
auto y = x[0 .. 2]; // allocates copy of stack on the heap
static assert(__traits(compiles, { chain(y, y); }));
static assert(!__traits(compiles, { chain(x, y); })); But, of course, it is not as efficient as it could be in the case when Is there some unsafe special case I've missed? If so, example please, @schveiguy. |
I agree with @schveiguy, this is not a good change. It overloads the meaning of |
Agreed with @quickfur and @schveiguy . |
Of course, not. In the example shown you just stay within the right stack frame. See this: import std.algorithm, std.range, std.stdio;
void blowup(T)(T range){
int[1024] stomp;
stomp[] = 0xFF;
foreach(v; range){
writeln(v);
}
}
auto theFuse(){
int[3] data = [1,2,3];
return chain(data[0..1],data[1..2],data[2..3]);
}
void main(){
blowup(theFuse());
} Output:
If we hide such horrid bugs behind built-in operator we would inflict severe damage to our reputation. |
@DmitryOlshansky You're right. My mistake. |
This allows to write more natural and readable code like:
instead of more noisy:
Publishing this at an early stage to get quick feedback.
For details see
http://forum.dlang.org/thread/cxgrlifaovdlwzbwsmsp@forum.dlang.org#post-bcazbwztlspwzpvlrblm:40forum.dlang.org
One question:
Should
evaluate to a single instance of
chain
asor to
?