digitalmars.D - More tricky range semantics
- H. S. Teoh via Digitalmars-d (94/94) Jan 15 2015 Just noticed this little gotcha today: suppose we have a forward range
- Andrei Alexandrescu (3/6) Jan 15 2015 That's right. To simplify the problem space we might decree that forward...
- H. S. Teoh via Digitalmars-d (40/47) Jan 15 2015 That's not workable. It instantly makes InputRangeObject useless.
- Andrei Alexandrescu (3/12) Jan 15 2015 Indeed. That said we could achieve polymorphism with value objects; it's...
- Joseph Rushton Wakeling via Digitalmars-d (3/5) Jan 15 2015 That would seem to place significant restrictions on the ability to defi...
- Andrei Alexandrescu (3/9) Jan 15 2015 Agreed. Pseudo-random generators are an interesting kind of range
- H. S. Teoh via Digitalmars-d (25/38) Jan 15 2015 I've been wondering about that. Must ranges have an "underlying"
- Joseph Rushton Wakeling via Digitalmars-d (14/26) Jan 16 2015 This is almost a surprising question for me, because I've taken it for g...
- Dicebot (3/11) Jan 16 2015 Why RNG would be a forward range as opposed to just input range?
- Dicebot (4/16) Jan 16 2015 To specify : the way I see it you either want PRNG to be a
- Joseph Rushton Wakeling (51/54) Jan 16 2015 Here's the problem with value semantics. This is a real problem
- Tobias Pankrath (7/25) Jan 16 2015 While the first example is indeed problematic, this one actually
- Joseph Rushton Wakeling (11/16) Jan 16 2015 I think you've misunderstood what I was getting at, probably
- Joseph Rushton Wakeling (13/15) Jan 16 2015 There's a concrete example of the problem I can demonstrate from
- Tobias Pankrath (8/25) Jan 16 2015 Ah, now I understand you. Since copy-construction is undefined
- Joseph Rushton Wakeling (4/10) Jan 16 2015 I don't see how that would affect the undesirable behaviour of
- Tobias Pankrath (14/21) Jan 16 2015 If you pass a forward range of type T (something that passes
- H. S. Teoh via Digitalmars-d (27/47) Jan 16 2015 IIRC, this is because the test checks if it's possible to assign the
- H. S. Teoh via Digitalmars-d (37/45) Jan 15 2015 Not to mention, I just realized that while the example I used relied on
- Andrei Alexandrescu (4/9) Jan 15 2015 There's a distinction here. Input non-forward ranges can be considered
- H. S. Teoh via Digitalmars-d (15/25) Jan 15 2015 I hope you realize that inputRangeObject, in spite of its name, does
- Tobias Pankrath (1/5) Jan 16 2015 This is related: https://issues.dlang.org/show_bug.cgi?id=11951
Just noticed this little gotcha today: suppose we have a forward range R, and an algorithm that wraps around R that looks something like this: struct WrapperRange { R src; bool stoppingCondition; this(R _src) { src = _src; stoppingCondition = someCondition(src.front); } property bool empty() { return stoppingCondition; } property auto front() { return src.front; } void popFront() { src.popFront(); if (!src.empty && someCondition(src.front)) stoppingCondition = true; } property auto save() { typeof(this) copy = this; copy.src = src.save; return copy; } } Basically, the wrapper iterates over R with some stopping condition that may cause it to end earlier than the actual end of R. Now consider this innocuous-looking code: unittest { R r = ... /* get an instance of R */; auto wrapper = WrapperRange(r); // check that the contents of wrapper is what we expect assert(wrapper.equal(expectedData)); // erhm... check the contents of wrapper again, just to // be doubly sure? assert(wrapper.equal(expectedData)); // <-- line 10 } You may laugh at line 10, marked above... but don't be so quick to laugh just yet. Consider now if R is a reference type. What would happen? Let's trace the steps: - The "R r = ..." line is nothing unexpected: since R is a by-reference type, r simply stores a reference to the actual instance of R. Nothing surprising. - The "auto wrapper" line creates an instance of WrapperRange -- which is by-value, btw, this will become important later -- and assigns to .src the reference to that instance of R. - The next line calls equal() on wrapper, to verify that its contents are what we're expecting. This passes a copy of wrapper to equal(), because wrapper is a by-value type. But since R is a reference type, as equal() iterates over the copy of wrapper, the underlying R range is being popped. This has 2 consequences: 1) When equal() returns, the original copy of wrapper no longer points to the same place in r as before, because equal() has popped the underlying instance of R. 2) Furthermore, since equal() iterates over the entire wrapped range, .stoppingCondition is now true. However, this only affected the local copy of wrapper in equal(). This means that after equal() returns, the unittest's original copy of wrapper is now in an inconsistent state: r has reached an element for which someCondition() is true, but this is not reflected in wrapper, which still thinks it's in the original position in r as before the call to equal(). As a result, the assert on line 10 will fail. This situation is quite counterintuitive. One would expect that either: (i) wrapper has value semantics, meaning that passing it to equal() would not consume it; or, (ii) wrapper has reference semantics, meaning that passing it to equal() would empty it. However, what actually happens is that wrapper is left in an inconsistent state upon returning from equal() that is neither (i) nor (ii). The kicker is that this resulted from code that followed the range API to the letter. No hidden loophole in the range API was exploited. No tricky hacks were used that might have caused problems. So what's the moral of the story? a) At least as things currently stand, passing (wrapper) ranges around may exhibit "undefined" behaviour, like the above. Passing a range to a function may invalidate it unless you use .save. Therefore, one should *always* use .save. (If we had passed wrapper.save to equal() instead, this problem would not have happened.) This applies even if the wrapper range is a by-value type. Or should we say, *especially* when it's a by-value type? b) One may argue that WrapperRange ought to .save the underlying range in its postblit... but what if the only thing we wanted to do was to call equal() on wrapper and nothing else? In that case, we'd be incurring needless overhead of .save'ing the range when we didn't care whether it got consumed. c) This issue is already latent in almost *all* Phobos algorithms. We only haven't discovered it yet because most people just use arrays for ranges. But all it takes is for somebody to start using std.range.inputRangeObject (and there are cases where this is necessary), and this problem will surface. Anytime you mix by-value and by-reference ranges, be ready for problems of this sort to arise. Yep, range semantics just got even trickier. (And we thought transient ranges were bad...) T -- "Computer Science is no more about computers than astronomy is about telescopes." -- E.W. Dijkstra
Jan 15 2015
On 1/15/15 12:53 PM, H. S. Teoh via Digitalmars-d wrote:Passing a range to a function may invalidate it unless you use .save. Therefore, one should *always* use .save.That's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- Andrei
Jan 15 2015
On Thu, Jan 15, 2015 at 03:24:29PM -0800, Andrei Alexandrescu via Digitalmars-d wrote:On 1/15/15 12:53 PM, H. S. Teoh via Digitalmars-d wrote:That's not workable. It instantly makes InputRangeObject useless. InputRangeObject is required in situations where run-time polymorphism of ranges is needed, for example, if a particular function must return one of multiple possible range types depending on a runtime parameter. Simple example: auto wrapMyRange(R)(R range) { if (someRuntimeCondition) { return range.map!(a => a*2); } else { return range.map!(a => a*2 + 1) .filter!(a > 10); } } This code doesn't compile, of course, because the return types of map() and filter() are incompatible. The only current way to make it work is to wrap the return value in an InputRangeObject: auto wrapMyRange(R)(R range) { if (someRuntimeCondition) { return inputRangeObject(range.map!(a => a*2)); } else { return inputRangeObject(range.map!(a => a*2 + 1) .filter!(a > 10)); } } This works because inputRangeObject returns an instance of a subclass of InputRangeObject, which serves as the common return type. The concrete type is specialized for the actual type being wrapped; in this case either the return type of map(), or the return type of filter(). Note that methods like .save, .popBack, etc., are forwarded, so the returned range retains forward or higher range functionality. This is, of course, necessary, since otherwise you couldn't do anything with the returned wrapped range except iterate over it once. Forcing reference type ranges to be non-forward completely breaks this important use case. The only way to work around it would be to wrap the class object in a struct wrapper that calls .save in its postblit -- which introduces a prohibitive performance overhead. T -- Let's not fight disease by killing the patient. -- Sean 'Shaleh' PerryPassing a range to a function may invalidate it unless you use .save. Therefore, one should *always* use .save.That's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- Andrei
Jan 15 2015
On 1/15/15 4:46 PM, H. S. Teoh via Digitalmars-d wrote:On Thu, Jan 15, 2015 at 03:24:29PM -0800, Andrei Alexandrescu via Digitalmars-d wrote:Indeed. That said we could achieve polymorphism with value objects; it's just more complicated. I agree it's a tricky matter. -- AndreiOn 1/15/15 12:53 PM, H. S. Teoh via Digitalmars-d wrote:That's not workable. It instantly makes InputRangeObject useless.Passing a range to a function may invalidate it unless you use .save. Therefore, one should *always* use .save.That's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- Andrei
Jan 15 2015
On 16/01/15 00:24, Andrei Alexandrescu via Digitalmars-d wrote:That's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- AndreiThat would seem to place significant restrictions on the ability to define effective random number generators and related functionality ... ?
Jan 15 2015
On 1/15/15 4:58 PM, Joseph Rushton Wakeling via Digitalmars-d wrote:On 16/01/15 00:24, Andrei Alexandrescu via Digitalmars-d wrote:Agreed. Pseudo-random generators are an interesting kind of range because they are forward yet do not iterate a "real" container. -- AndreiThat's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- AndreiThat would seem to place significant restrictions on the ability to define effective random number generators and related functionality ... ?
Jan 15 2015
On Thu, Jan 15, 2015 at 05:52:45PM -0800, Andrei Alexandrescu via Digitalmars-d wrote:On 1/15/15 4:58 PM, Joseph Rushton Wakeling via Digitalmars-d wrote:I've been wondering about that. Must ranges have an "underlying" container? Or are they allowed to be more abstract entities that basically function as generators, producing data on demand? (This distinction is also somewhat related to transient ranges, in that the difference between providing a "view" of some static underlying data vs. computing something on-the-fly in a buffer that gets reused, could serve as a deciding factor on how to deal with transient ranges.) My feeling is that allowing the latter is much more powerful, and more encompassing. Constructs like iota() and recurrence() belong to the latter category, for example, and they do provide forward range like functionality -- or they *could*, if they don't already, as it would be trivial to implement. Allowing generating functions to be ranges also allows one to plug in programmatic data generators as data sources in UFCS chains, without needing to deal with an artificial distinction between "view of underlying data" ranges and "generated-on-the-fly" ranges. In this sense, an RNG could be thought of as a complex variant of recurrence(), except with a far more complicated generating expression than what one would normally use with recurrence(). If recurrence() qualifies as a range (and a forward range, no less), then an RNG ought to qualify too. T -- Those who don't understand Unix are condemned to reinvent it, poorly.On 16/01/15 00:24, Andrei Alexandrescu via Digitalmars-d wrote:Agreed. Pseudo-random generators are an interesting kind of range because they are forward yet do not iterate a "real" container. -- AndreiThat's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- AndreiThat would seem to place significant restrictions on the ability to define effective random number generators and related functionality ... ?
Jan 15 2015
On 16/01/15 07:38, H. S. Teoh via Digitalmars-d wrote:I've been wondering about that. Must ranges have an "underlying" container? Or are they allowed to be more abstract entities that basically function as generators, producing data on demand? (This distinction is also somewhat related to transient ranges, in that the difference between providing a "view" of some static underlying data vs. computing something on-the-fly in a buffer that gets reused, could serve as a deciding factor on how to deal with transient ranges.)This is almost a surprising question for me, because I've taken it for granted for so long that one of the most powerful tools they offer is lazily-generated data that is not based on underlying storage. The examples you cite (iota, recurrence) are good examples, RNGs are another.In this sense, an RNG could be thought of as a complex variant of recurrence(), except with a far more complicated generating expression than what one would normally use with recurrence(). If recurrence() qualifies as a range (and a forward range, no less), then an RNG ought to qualify too.Yes, theoretically you can envision a pseudo-RNG as being defined by a state variable, and a pure function that transforms this into a new state variable. And then those individual states can be mapped to the individual variates in different ways, depending on the type of variate you want to generate. The practical range implementation obfuscates this a bit (helpfully) by encapsulating the state (which in practice we update via mutation), and implementing a particular state-to-random-variate method in .front, resulting in the desired effect of a range whose elements are the individual random variates you care about.
Jan 16 2015
On Friday, 16 January 2015 at 00:58:34 UTC, Joseph Rushton Wakeling via Digitalmars-d wrote:On 16/01/15 00:24, Andrei Alexandrescu via Digitalmars-d wrote:Why RNG would be a forward range as opposed to just input range?That's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- AndreiThat would seem to place significant restrictions on the ability to define effective random number generators and related functionality ... ?
Jan 16 2015
On Friday, 16 January 2015 at 14:58:09 UTC, Dicebot wrote:On Friday, 16 January 2015 at 00:58:34 UTC, Joseph Rushton Wakeling via Digitalmars-d wrote:To specify : the way I see it you either want PRNG to be a forward range and that fits with value semantics. Or you want reference semantics and it naturally becomes input range.On 16/01/15 00:24, Andrei Alexandrescu via Digitalmars-d wrote:Why RNG would be a forward range as opposed to just input range?That's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- AndreiThat would seem to place significant restrictions on the ability to define effective random number generators and related functionality ... ?
Jan 16 2015
On Friday, 16 January 2015 at 14:59:10 UTC, Dicebot wrote:To specify : the way I see it you either want PRNG to be a forward range and that fits with value semantics. Or you want reference semantics and it naturally becomes input range.Here's the problem with value semantics. This is a real problem with actual Phobos code right now. //////////////////////////////////////////////////// import std.random, std.range, std.stdio; void main() { // Create RNG instance with unpredictable seed auto rng = Random(unpredictableSeed); // Generate sample of 3 numbers from sequence // 0, 1, ..., 9 using rng as source of randomness // and write to console iota(0, 10).randomSample(3, rng).writeln; // Do again. We'd expect a different result // but actually we get just the same iota(0, 10).randomSample(3, rng).writeln; } //////////////////////////////////////////////////// Note that because randomSample generated a wrapper range (RandomSample), we can't simply pass the RNG by ref. It's copied (and because RNGs are currently value types, it's copied by value). Note also that the above is a problem whether or not Random is a forward or input range. What's needed here is for the source of randomness to be updated whenever it's used, so that you don't get unintended correlations like this. Reference types give this, but it shouldn't be necessary to interfere with the forward-range status of the RNG, which depends entirely on whether it's a deterministic pseudo-RNG or not. Conversely, suppose that we have some function that takes a forward range as input and uses the ability to repeat the sequence. Here's a naive example: void foo (FRange) (FRange range) if (isForwardRange!FRange) { foreach (i; 0 .. 10) { // silly example :-P auto r = range.save; r.take(10).writeln; } } This is a problematic design if FRange is a reference type, because (by design) if the values in it are used, they should be consumed. So e.g. if you were passing a reference-type RNG to a function that does this, you'd like to guarantee that (i) the function is able to use the ability to repeat the sequence, but (ii) consumes from the original exactly once. If you don't get that, you will wind up with unintended correlations in your use of random numbers.
Jan 16 2015
On Friday, 16 January 2015 at 16:12:23 UTC, Joseph Rushton Wakeling wrote:void foo (FRange) (FRange range) if (isForwardRange!FRange) { foreach (i; 0 .. 10) { // silly example :-P auto r = range.save; r.take(10).writeln; } } This is a problematic design if FRange is a reference type, because (by design) if the values in it are used, they should be consumed. So e.g. if you were passing a reference-type RNG to a function that does this, you'd like to guarantee that (i) the function is able to use the ability to repeat the sequence, but (ii) consumes from the original exactly once. If you don't get that, you will wind up with unintended correlations in your use of random numbers.While the first example is indeed problematic, this one actually is not. If this does not print the same 10 numbers every time, your save method is wrong. Regardless of being a reference type or not it has to clone the RNG state or it doesn't do its job.
Jan 16 2015
On Friday, 16 January 2015 at 17:09:47 UTC, Tobias Pankrath wrote:While the first example is indeed problematic, this one actually is not. If this does not print the same 10 numbers every time, your save method is wrong. Regardless of being a reference type or not it has to clone the RNG state or it doesn't do its job.I think you've misunderstood what I was getting at, probably because I didn't explain myself well. Obviously it's correct that the second function example should print out the same thing 10 times. However, what is wrong is that at the end of that function, the source range -- if a reference type -- should itself have been popFront'ed a sufficient number of times, but it hasn't been. That's a design fault in the function implementation, which doesn't take into account the desirable behaviour (for the caller) if the range given to the function is a reference type.
Jan 16 2015
On Friday, 16 January 2015 at 17:13:33 UTC, Joseph Rushton Wakeling wrote:I think you've misunderstood what I was getting at, probably because I didn't explain myself well.There's a concrete example of the problem I can demonstrate from some Phobos functionality, but off the top of my head I can't remember what it is. I'll try and look it up when I get home later this evening. The essence is that, as a caller, I have this problem: auto range = SomeReferenceTypeForwardRange(whatever); foo(range); // prints one set of 10 values foo(range); // should print different set of 10 values // but won't because foo()'s implementation // doesn't take into account possibility of // reference type input
Jan 16 2015
On Friday, 16 January 2015 at 17:13:33 UTC, Joseph Rushton Wakeling wrote:On Friday, 16 January 2015 at 17:09:47 UTC, Tobias Pankrath wrote:Ah, now I understand you. Since copy-construction is undefined for ForwardRanges, you cannot guarantee this. Things would be better, if we had required that this(this) does the same as .save or must be disabled. Than it would be clear that you either had to use a reference or a pointer as argument to the function.While the first example is indeed problematic, this one actually is not. If this does not print the same 10 numbers every time, your save method is wrong. Regardless of being a reference type or not it has to clone the RNG state or it doesn't do its job.I think you've misunderstood what I was getting at, probably because I didn't explain myself well. Obviously it's correct that the second function example should print out the same thing 10 times. However, what is wrong is that at the end of that function, the source range -- if a reference type -- should itself have been popFront'ed a sufficient number of times, but it hasn't been. That's a design fault in the function implementation, which doesn't take into account the desirable behaviour (for the caller) if the range given to the function is a reference type.
Jan 16 2015
On Friday, 16 January 2015 at 17:22:42 UTC, Tobias Pankrath wrote:Ah, now I understand you. Since copy-construction is undefined for ForwardRanges, you cannot guarantee this. Things would be better, if we had required that this(this) does the same as .save or must be disabled.I'm not sure I follow what you mean by this ... ?Than it would be clear that you either had to use a reference or a pointer as argument to the function.I don't see how that would affect the undesirable behaviour of the implementation.
Jan 16 2015
On Friday, 16 January 2015 at 18:12:03 UTC, Joseph Rushton Wakeling wrote:On Friday, 16 January 2015 at 17:22:42 UTC, Tobias Pankrath wrote:If you pass a forward range of type T (something that passes isForwardRange!T) to a function you have no guarantees on what will happen. It could be: • compilation failure • reference semantics (changes are reflected outside the function • value semantics (no change are reflected outside the function) The implementer of the range can choose one. Although I tried it now, and disable this(this) does prevent isForwardRange!T to pass. I don't know if that changed recently or is just out of line with the documentation/specification.Ah, now I understand you. Since copy-construction is undefined for ForwardRanges, you cannot guarantee this. Things would be better, if we had required that this(this) does the same as .save or must be disabled.I'm not sure I follow what you mean by this ... ?
Jan 16 2015
On Fri, Jan 16, 2015 at 06:34:08PM +0000, Tobias Pankrath via Digitalmars-d wrote:On Friday, 16 January 2015 at 18:12:03 UTC, Joseph Rushton Wakeling wrote:* The range is left in an inconsistent state. :-)On Friday, 16 January 2015 at 17:22:42 UTC, Tobias Pankrath wrote:If you pass a forward range of type T (something that passes isForwardRange!T) to a function you have no guarantees on what will happen. It could be: • compilation failure • reference semantics (changes are reflected outside the function • value semantics (no change are reflected outside the function)Ah, now I understand you. Since copy-construction is undefined for ForwardRanges, you cannot guarantee this. Things would be better, if we had required that this(this) does the same as .save or must be disabled.I'm not sure I follow what you mean by this ... ?The implementer of the range can choose one. Although I tried it now, and disable this(this) does prevent isForwardRange!T to pass. I don't know if that changed recently or is just out of line with the documentation/specification.IIRC, this is because the test checks if it's possible to assign the range to a local variable. This is quite widely used in Phobos; disable this(this) would make the range unusable with a *lot* of Phobos algorithms. In fact, pretty much *all* wrapper algorithms that have to assign the range to a field in the wrapper range. Besides, I think this approach doesn't really address the root of the problem, which is that the semantics of assigning a range (without using .save) is not specified by the range API. Yet code like `auto r = range;` is used all over the place in Phobos code, with various hidden assumptions about what '=' does, which may or may not correspond with reality. Ultimately, this is also the root cause of the transient range problem: the range API did not specify the semantics of `auto x = range.front;`. It's simply assumed that this copies the value of range.front... which, in a sense, it does, but what's hidden behind the '=' can be very complex semantics that breaks the algorithm's assumptions. In this case, it breaks because when range.front is a reference to a buffer reused by popFront, this causes the unexpected result that calling popFront also changes x. Had the range API specified the expected behaviour of assigning .front to a variable, this problem would not have arisen, or at least the possible problems would have been anticipated, instead of transience coming to bite us from behind when we least expect it. T -- I think the conspiracy theorists are out to get us...
Jan 16 2015
On Fri, Jan 16, 2015 at 01:58:15AM +0100, Joseph Rushton Wakeling via Digitalmars-d wrote:On 16/01/15 00:24, Andrei Alexandrescu via Digitalmars-d wrote:Not to mention, I just realized that while the example I used relied on the interaction between reference and value types, a similar problem exists with just value types alone. Using the same wrapper range I posted, suppose now that R is not a reference type range, but an input (non-forward) range, and that .save is not implemented. Then consider this code: unittest { auto r = R(...); auto wrapped = WrapperRange(r); assert(wrapped.equal(expectedData)); bool b = wrapped.empty; } Quiz: what's the value of b? . . . Go on, guess. :-) . . . Yup, b == false. Now guess what happens if you then access wrapped.front? Well, that depends. If someCondition became true before r was exhausted, then wrapped.front would return a value that shouldn't be in the wrapped range, because it breaks the invariant that WrapperRange is supposed to stop when someCondition becomes true. However, if someCondition didn't become true and r was exhausted, accessing wrapped.front will call .front on an empty input range, which is UB. If you're lucky, you'll hit an assert; otherwise, anything could happen, like dereferencing a dangling pointer, reading invalid memory, etc.. So actually, my previous statement was not broad enough: this problem happens not just with reference type ranges; it happens with non-reference input ranges too! T -- If it breaks, you get to keep both pieces. -- Software disclaimer noticeThat's right. To simplify the problem space we might decree that forward (or better) ranges with reference semantics are not allowed. -- AndreiThat would seem to place significant restrictions on the ability to define effective random number generators and related functionality ... ?
Jan 15 2015
On 1/15/15 12:53 PM, H. S. Teoh via Digitalmars-d wrote:This issue is already latent in almost*all* Phobos algorithms. We only haven't discovered it yet because most people just use arrays for ranges. But all it takes is for somebody to start using std.range.inputRangeObject (and there are cases where this is necessary), and this problem will surface.There's a distinction here. Input non-forward ranges can be considered "reference" because popFront()ing any copy is tantamount to popFront()int any other copy. -- Andrei
Jan 15 2015
On Thu, Jan 15, 2015 at 03:25:38PM -0800, Andrei Alexandrescu via Digitalmars-d wrote:On 1/15/15 12:53 PM, H. S. Teoh via Digitalmars-d wrote:I hope you realize that inputRangeObject, in spite of its name, does forward methods of the higher ranges (.save, .popBack, etc.), right? Besides, conflating reference types with non-forward input ranges will cripple ranges built not only from class objects, but from *any* type (even structs) that exhibit reference semantics. One particular poignant example is your proposed groupBy replacement, which uses RefCounted, which has reference semantics. :-) We wouldn't want to be breaking that now, would we? (On the flip side, perhaps now you might finally see some justification for my hesitation about implementing groupBy with reference semantics...) T -- Let's eat some disquits while we format the biskettes.This issue is already latent in almost*all* Phobos algorithms. We only haven't discovered it yet because most people just use arrays for ranges. But all it takes is for somebody to start using std.range.inputRangeObject (and there are cases where this is necessary), and this problem will surface.There's a distinction here. Input non-forward ranges can be considered "reference" because popFront()ing any copy is tantamount to popFront()int any other copy. -- Andrei
Jan 15 2015
Yep, range semantics just got even trickier. (And we thought transient ranges were bad...) TThis is related: https://issues.dlang.org/show_bug.cgi?id=11951
Jan 16 2015