Bug 12583 – Allow user defined "retro" range

Status
NEW
Severity
enhancement
Priority
P4
Component
phobos
Product
D
Version
D2
Platform
All
OS
All
Creation time
2014-04-15T13:53:09Z
Last change time
2024-12-01T16:20:48Z
Assigned to
No Owner
Creator
monarchdodra
Moved to GitHub: phobos#9630 →

Comments

Comment #0 by monarchdodra — 2014-04-15T13:53:09Z
I have a use case, where I have a range [*] "Range myRange" whose behavior is biased in favor of forwards iteration. Backwards iteration is also possible, but more costly. That said, I have a RetroRange, that iterates the same thing as my Range, but in reverse order, and more efficiently than what I'd get with "retro(myRange)". I've implemented the "retro()" member functions on both these ranges, so flipping iteration scheme is as simple as calling ".retro()". I think the "retro()" member function should be a range trait that is recognized by std.range.retro. This way, one can write either of "myRange.retro()" or "retro(myRange)" and have the same, customized high performance result. -------- [*] These kinds of range are usually those that iterate over containers, where it is most efficient to store _first and _pastTheEnd elements. In particular, linked list or binary trees. Other ranges could also benefit from this approach. For example, zip: finding the back of zip is no trivial operation. However, a type specifically designed to zip backwards could be *much* more efficient.
Comment #1 by schveiguy — 2014-04-15T16:11:42Z
Fully agree with this. In fact, retro could accept any type that supports .retro(), even non-ranges. I wonder if foreach_reverse could be specialized to call this on bidirectional ranges... Note, this is a similar pattern to 'put', I wonder if there is a commonality here we can extract into a "D design pattern".
Comment #2 by issues.dlang — 2014-04-17T09:34:22Z
I think that having std.range.retro call the member function retro if there is one is fine, but I should point out that this is a general problem which is not at all specific to retro. Pretty much _any_ free function can be replaced with a member function which is more efficient if a user-defined type has a way of doing the same thing more efficiently. So, to solve this problem in the general case, I think we have to do one of two things: 1. Make it standard policy to have a free function check if there is a member function which takes the same arguments and then calls that instead of using its own implementation if such a function exists. 2. Make it standard policy to always use UFCS, in which case, the member function will always be used if it exists. In general, I would argue simply for going with #2, because it's less of a maintenance nightmare, though doing #1 in addition to #2 would catch the cases when someone fails to do #2. Still, I really think that this problem is simply an argument for why UFCS should always be used where possible. And personally, I think that this problem is really the only reason that UFCS is worth having. All of the other reasons for it that I can think of are purely subjective, where in this case, it actually solves a real problem to use it.
Comment #3 by monarchdodra — 2014-04-17T13:00:28Z
(In reply to Jonathan M Davis from comment #2) > I think that having std.range.retro call the member function retro if there > is one is fine, but I should point out that this is a general problem which > is not at all specific to retro. Pretty much _any_ free function can be > replaced with a member function which is more efficient if a user-defined > type has a way of doing the same thing more efficiently. So, to solve this > problem in the general case, I think we have to do one of two things: > > 1. Make it standard policy to have a free function check if there is a > member function which takes the same arguments and then calls that instead > of using its own implementation if such a function exists. > > 2. Make it standard policy to always use UFCS, in which case, the member > function will always be used if it exists. The issue is that 2 can clash with 1, if the type in question has the member function, but not with the correct args. A typical example is `put`: It should *not* be used UFCS: //---- struct S { void put(int){}; } void main() { S s; int[] a; put(s, a); //Fine, put the array a into s. s.put(a); //Nope, don't know how to. } //---- So, if (for some strange reason), I were to add "retro(int)" to my function, but not "retro()", then a UFCS call to "myRange.retro()" would fail.
Comment #4 by issues.dlang — 2014-04-18T12:56:23Z
If myRange.retro() fails, because typeof(myRange) defined retro(int) but not retro, then that sounds like a bug with UFCS to me. retro(int) doesn't match myRange.retro(), but std.range.retro does, so std.range.retro should be called rather than the member function. If a member function has a different signature than the free function, then it's not intended as a replacement for the free function, and it shouldn't be conflicting with the free function at all.
Comment #5 by schveiguy — 2014-04-18T13:17:03Z
(In reply to Jonathan M Davis from comment #4) > If a member function has a different signature than the free function, then > it's not intended as a replacement for the free function, and it shouldn't > be conflicting with the free function at all. No, it's not a bug. Lookup rules work on overload sets, not on individual functions. If you define a ufcs method that overloads with a straight method, you cannot call the ufcs method, this is intended.
Comment #6 by issues.dlang — 2014-04-20T04:11:12Z
> No, it's not a bug. Lookup rules work on overload sets, not on individual > functions. If you define a ufcs method that overloads with a straight method, > you cannot call the ufcs method, this is intended. I'm not sure that I agree that that's good behavior, but given how prevalent UFCS is and how many consider it good practice to use it rather than the normal function call syntax, it seems like it's a bad idea to name any member functions with a name which is known to conflict with a free function that is likely to be used with that type. And given that unless we use UFCS as a matter of course, there really isn't a way for types to provide specialized implementations for well-known free functions (such as retro), I'm very much inclined to argue that we should just be using UFCS as much as possible and that users should be advised not to name their member functions with names which are likely to conflict with Phobos functions which are likely to be used with their types. Though honestly, I'm inclined to argue that erroring out on foo.retro() because typeof(foo) defines retro(int) is ultimately a bad idea. It risks making UFCS a nightmare to use. And I have a hard time believing that it's going to scale very well to have templated free functions check to see whether the type that they're being instantiated with declared a member function which matches. There's nothing special about retro. It's just one case among many where it would make sense for a type to provide a specialization for a well-known free function. Certainly, it seems to me that this situation is exactly what UFCS was designed for. You call the function without caring whether it's a free function or a member function, and it just works. If the overloading rules cause problems with that, then we have a serious problem IMHO, and personally, I think that this case is the main reason why UFCS makes any objective sense at all - i.e. making it so that you can call a function without caring about whether it was defined as a free function or a member function. Every other reason to use UFCS is subjective.
Comment #7 by schveiguy — 2014-04-25T02:24:01Z
I've altered my opinion slightly on this. See: http://forum.dlang.org/post/[email protected] Long story short, I think this is still a good idea, but the member function should NOT be named retro.
Comment #8 by robert.schadek — 2024-12-01T16:20:48Z
THIS ISSUE HAS BEEN MOVED TO GITHUB https://github.com/dlang/phobos/issues/9630 DO NOT COMMENT HERE ANYMORE, NOBODY WILL SEE IT, THIS ISSUE HAS BEEN MOVED TO GITHUB