www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Problem with C++ ranges also exhibited by D

reply Atila Neves <atila.neves gmail.com> writes:
This blog post shows off some problems with the ranges v3 library 
that is going to be included in the C++20 standard library:

https://www.fluentcpp.com/2019/04/16/an-alternative-design-to-iterators-and-ranges-using-stdoptional/

Because the problem is basically (from the blog) "Is it really 
necessary to have separate operations for advancing the iterator 
and evaluating its element?". The answer is of course no, and 
this has been brought up in the forums before. Like it or not, D 
has the same issue:

-----------------------------
import std.algorithm;
import std.range;
import std.stdio;

void main() {
     iota(1, 6)
         .map!((n) { writeln("transform ", n); return n * 2; })
         .filter!(n => n % 4 == 0)
         .writeln;
}
-----------------------------

Produces the output:

[transform 1
transform 2
transform 2
4transform 3
transform 4
, transform 4
8transform 5
]

Showing that the mapped function is called more times than 
strictly necessary, just as in C++.
Apr 16
next sibling parent reply Dukc <ajieskola gmail.com> writes:
On Tuesday, 16 April 2019 at 12:07:03 UTC, Atila Neves wrote:
 This blog post shows off some problems with the ranges v3 
 library that is going to be included in the C++20 standard 
 library.
I belive the current design is superior, because of the ease of doing this: import std.algorithm; import std.range; import std.stdio; void main() { iota(1, 6) .map!((n) { writeln("transform ", n); return n * 2; }) .cache .filter!(n => n % 4 == 0) .writeln; } After all, sometimes you might want front() to be lazy. With the current design, you have the choice.
Apr 16
parent Atila Neves <atila.neves gmail.com> writes:
On Tuesday, 16 April 2019 at 12:47:51 UTC, Dukc wrote:
 On Tuesday, 16 April 2019 at 12:07:03 UTC, Atila Neves wrote:
 This blog post shows off some problems with the ranges v3 
 library that is going to be included in the C++20 standard 
 library.
I belive the current design is superior, because of the ease of doing this: import std.algorithm; import std.range; import std.stdio; void main() { iota(1, 6) .map!((n) { writeln("transform ", n); return n * 2; }) .cache .filter!(n => n % 4 == 0) .writeln; } After all, sometimes you might want front() to be lazy. With the current design, you have the choice.
I have to confess my ignorance on the existence of `cache`. Huh. Thanks!
Apr 17
prev sibling next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Tue, Apr 16, 2019 at 12:07:03PM +0000, Atila Neves via Digitalmars-d wrote:
 This blog post shows off some problems with the ranges v3 library that
 is going to be included in the C++20 standard library:
 
 https://www.fluentcpp.com/2019/04/16/an-alternative-design-to-iterators-and-ranges-using-stdoptional/
 
 Because the problem is basically (from the blog) "Is it really
 necessary to have separate operations for advancing the iterator and
 evaluating its element?". The answer is of course no, and this has
 been brought up in the forums before.
Actually, I've used libraries in the past that combine advancing the range with reading the next element. There's certainly a benefit when you're doing simple iteration and don't want to bother to have to manually bump the range everywhere. However, I found that for non-trivial tasks I end up sprinkling ad hoc caching code everywhere, just because reading the front of a range is conflated with advancing it. One of the most annoying examples of this sort is the Posix file API, where .eof is uncheckable until you perform a read operation, and read may arbitrarily block, resulting in a percolation of corner cases, workarounds, and other band-aid just to make it work nicely with code that needs to check EOF without blocking / consuming the front of the data. For this reason, I found D's range API much cleaner to use, despite being more verbose. D's range API puts the onus on the range author to write correct code to retain the value of .front; the other API forces the client code to cache values when needed, violating DRY, and client code doesn't always do it right.
 Like it or not, D has the same issue:
 
 -----------------------------
 import std.algorithm;
 import std.range;
 import std.stdio;
 
 void main() {
     iota(1, 6)
         .map!((n) { writeln("transform ", n); return n * 2; })
         .filter!(n => n % 4 == 0)
         .writeln;
 }
 -----------------------------
 
 Produces the output:
 
 [transform 1
 transform 2
 transform 2
 4transform 3
 transform 4
 , transform 4
 8transform 5
 ]
 
 Showing that the mapped function is called more times than strictly
 necessary, just as in C++.
[...] Just use .cache and move on already! ;-) T -- In theory, software is implemented according to the design that has been carefully worked out beforehand. In practice, design documents are written after the fact to describe the sorry mess that has gone on before.
Apr 16
prev sibling next sibling parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 04/16/2019 05:07 AM, Atila Neves wrote:
 "Is it really necessary
 to have separate operations for advancing the iterator and evaluating
 its element?".
I've been under the impression that such separation is for strong exception guarantee. We can't popFront (and C++ cannot operator++) before knowing that the copy of the returned value has not thrown an exception. The article you've linked does not even mention exceptions so perhaps this concern is now historical? Or perhaps there are no types that throw during copy anymore, meaning that they are either trivial bits or reference types? Ali
Apr 16
parent Atila Neves <atila.neves gmail.com> writes:
On Tuesday, 16 April 2019 at 17:14:17 UTC, Ali Çehreli wrote:
 On 04/16/2019 05:07 AM, Atila Neves wrote:
 "Is it really necessary
 to have separate operations for advancing the iterator and
evaluating
 its element?".
I've been under the impression that such separation is for strong exception guarantee. We can't popFront (and C++ cannot operator++) before knowing that the copy of the returned value has not thrown an exception. The article you've linked does not even mention exceptions so perhaps this concern is now historical? Or perhaps there are no types that throw during copy anymore, meaning that they are either trivial bits or reference types? Ali
That's a good point. I doubt the concern is historical, maybe the author just didn't know about it.
Apr 17
prev sibling parent aliak <something something.com> writes:
On Tuesday, 16 April 2019 at 12:07:03 UTC, Atila Neves wrote:
 This blog post shows off some problems with the ranges v3 
 library that is going to be included in the C++20 standard 
 library:

 https://www.fluentcpp.com/2019/04/16/an-alternative-design-to-iterators-and-ranges-using-stdoptional/

 Because the problem is basically (from the blog) "Is it really 
 necessary to have separate operations for advancing the 
 iterator and evaluating its element?". The answer is of course 
 no, and this has been brought up in the forums before. Like it 
 or not, D has the same issue:

 -----------------------------
 import std.algorithm;
 import std.range;
 import std.stdio;

 void main() {
     iota(1, 6)
         .map!((n) { writeln("transform ", n); return n * 2; })
         .filter!(n => n % 4 == 0)
         .writeln;
 }
 -----------------------------

 Produces the output:

 [transform 1
 transform 2
 transform 2
 4transform 3
 transform 4
 , transform 4
 8transform 5
 ]

 Showing that the mapped function is called more times than 
 strictly necessary, just as in C++.
You can implement map to not do that as well: https://run.dlang.io/is/9a2ba4 Take that with a grain of salt though, no idea what kind of corner cases the actual map from phobos deals with. Also, Ali pointed out strong exception guarantees. There's an article form Hurb Sutter on implementing a generic stack container [0] that explains this scenario well. It basically boils down to, if you have advance and evaluate in one function, it makes it impossible/awkward for the caller to write exception correct code. This pattern in particular (code to match the article link you posted) try { auto element = range.next; // Do something with element } catch (Exception ex) { // meh, ignore } if copying/assigning can throw, your top element is lost. Cheers, - Ali [0] http://ptgmedia.pearsoncmg.com/imprint_downloads/informit/aw/meyerscddemo/DEMO/MAGAZINE/SU_DIR.HTM#dingp39
Apr 16