Issue 8573 - A simpler Phobos function that returns the index of the …#4921
Issue 8573 - A simpler Phobos function that returns the index of the …#4921wilzbach merged 1 commit intodlang:masterfrom
Conversation
|
There doesn't seem to be a version of |
|
|
||
| Returns: | ||
| The position of the first encounter of the minimum (respectively maximum) in `range`. | ||
| */ |
std/algorithm/searching.d
Outdated
| return min_pos; | ||
| } | ||
|
|
||
| ///Ditto |
|
I added an alternate idea using |
| assert(minPos!("a[0] < b[0]")(b) == [ [2, 4], [4], [4] ]); | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
index of the first occurrence...
| maximum) element. | ||
| range = The input range to search. | ||
|
|
||
| Returns: |
There was a problem hiding this comment.
I think index is better than position, and we should mention -1 when the range is empty.
| is(typeof(binaryFun!pred(range.front, range.front)))) | ||
| { | ||
| if (range.empty) return -1; | ||
|
|
std/algorithm/searching.d
Outdated
| auto min = range.front(); | ||
| for (range.popFront(); !range.empty; range.popFront()) | ||
| { | ||
| ++cur_pos; |
There was a problem hiding this comment.
front should not have () parentheses - ditto for min initialisation.
It might be better to avoid copying range.front here.
|
Using mutable |
|
Re: my original comment about precedent with |
burner
left a comment
There was a problem hiding this comment.
The given range should be a forward range otherwise the index is useless as there is no more range to index.
std/algorithm/searching.d
Outdated
| Returns: | ||
| The position of the first encounter of the minimum (respectively maximum) in `range`. | ||
| */ | ||
| int getMinPos(alias pred = "a < b", Range)(Range range) |
There was a problem hiding this comment.
InputRange feels wrong here. You might get the index of the min element but the elements are gone.
ForwardRange IMO is the correct choice here.
There was a problem hiding this comment.
InputRange is a struct so it is passed by value. The range is consumed inside the function, but when it returns, there won't be any problems.
There was a problem hiding this comment.
InputRange is a struct so it is passed by value. The function does not consume the range from the caller context.
|
Do we really need a new function for this? [2, 4, 1].enumerate.maxElement!`a.value`.index; // 1or even shorter with the array syntax: [2, 4, 1].enumerate.maxElement!`a[1]`[0]; // 1Heck we can even use [2, 4, 1].enumerate.maxPos!`a.value`[0].index - 1; // 1I thought there was a reluctance against adding one-liners to Phobos. |
| ptrdiff_t minPosIndex(alias pred = "a < b", Range)(Range range) | ||
| if (isInputRange!Range && !isInfinite!Range && | ||
| is(typeof(binaryFun!pred(range.front, range.front)))) | ||
| { |
There was a problem hiding this comment.
For efficiency in release mode, minElement (using private extremum) assumes the range is not empty:
assert(!r.empty, "r is an empty range");
In that case we can use size_t instead of ptrdiff_t. (Forgot to mention this yesterday).
std/algorithm/searching.d
Outdated
|
|
||
| ptrdiff_t min_pos = 0, cur_pos = 0; | ||
| auto min = range.front; | ||
| Unqual!(typeof(range.front)) min = range.front; |
There was a problem hiding this comment.
Needs a unittest for const front.
There was a problem hiding this comment.
which will fail, as min needs to be a Rebindable
|
If I assign a task to bootcamp it means I put it up for review of the bootcamper, but not necessarily that the resolution of the bug would mean add a new artifact. In this case people look for the index of extrema all the time and all of our alternatives are roundabout. I'll allow this. |
andralex
left a comment
There was a problem hiding this comment.
So let's get this in good shape and merge.
| Returns: | ||
| The index of the first encounter of the minimum (respectively maximum) in `range`. If the | ||
| range is empty, -1 is returned. | ||
| */ |
There was a problem hiding this comment.
ssize_t isn't a recognized type. If you meant size_t, then this won't work since size_t is an uint and we need to return -1 when the range is empty.
There was a problem hiding this comment.
My bad. It seems that it does pass the unit test and it returns -1 even if return type is size_t. I assume it implicitly converts -1 to size_t, right?
There was a problem hiding this comment.
Yeah, that's not quite what you want. ssize_t is used in druntime. I guess we can use Signed!size_t.
There was a problem hiding this comment.
Ok, got it. Thanks
std/algorithm/searching.d
Outdated
| The index of the first encounter of the minimum (respectively maximum) in `range`. If the | ||
| range is empty, -1 is returned. | ||
| */ | ||
| ptrdiff_t minPosIndex(alias pred = "a < b", Range)(Range range) |
There was a problem hiding this comment.
Require a forward range. Input ranges are odd, e.g. if you copy the front of an input range that may be overwritten by advancing the input range (see byLine).
std/algorithm/searching.d
Outdated
| range is empty, -1 is returned. | ||
| */ | ||
| ptrdiff_t minPosIndex(alias pred = "a < b", Range)(Range range) | ||
| if (isInputRange!Range && !isInfinite!Range && |
There was a problem hiding this comment.
is(typeof(binaryFun!pred(range.front, range.front))) == bool)
| ptrdiff_t minPosIndex(alias pred = "a < b", Range)(Range range) | ||
| if (isInputRange!Range && !isInfinite!Range && | ||
| is(typeof(binaryFun!pred(range.front, range.front)))) | ||
| { |
std/algorithm/searching.d
Outdated
| { | ||
| if (range.empty) return -1; | ||
|
|
||
| ptrdiff_t min_pos = 0, cur_pos = 0; |
There was a problem hiding this comment.
Don't do this because not all types are copyable to their unqualified type. Use a second range that trails the first one and is positioned at the extremum found so far.
There was a problem hiding this comment.
I added a static if condition which copies only range.front when possible and uses a second range to trail the first range otherwise. Hope this is ok
| is(typeof(binaryFun!pred(range.front, range.front)))) | ||
| { | ||
| if (range.empty) return -1; | ||
|
|
There was a problem hiding this comment.
We don't use underscores in variable names, only in package and module names. Use thisConvention.
std/algorithm/searching.d
Outdated
| Unqual!(typeof(range.front)) min = range.front; | ||
| for (range.popFront(); !range.empty; range.popFront()) | ||
| { | ||
| ++cur_pos; |
| Returns: | ||
| The index of the first encounter of the minimum (respectively maximum) in `range`. If the | ||
| range is empty, -1 is returned. | ||
| */ |
| is(typeof(binaryFun!pred(range.front, range.front)) == bool)) | ||
| { | ||
| if (range.empty) return -1; | ||
|
|
std/algorithm/searching.d
Outdated
| { | ||
| if (range.empty) return -1; | ||
|
|
||
| Signed!size_t minPos = 0, curPos = 0; |
There was a problem hiding this comment.
Why not to store the element instead of range?
There was a problem hiding this comment.
The temporary is better here. Just use Unqual!(...) for min declaration
There was a problem hiding this comment.
The reason is lazy ranges and lazy algorithms. front should be called only once if possible
There was a problem hiding this comment.
That was my initial solution, but @andralex told me that not all types are copyable to their unqualified type.
There was a problem hiding this comment.
The common use case is auto amax = range.map!abs.maxIndex;. OK lets use .front
There was a problem hiding this comment.
I added a static if which only copies 1 element when the elements are copyable. Is that ok on your behalf?
std/algorithm/searching.d
Outdated
| return minPos; | ||
| } | ||
|
|
||
| ///Ditto |
| Returns: | ||
| The index of the first encounter of the minimum (respectively maximum) in `range`. If the | ||
| range is empty, -1 is returned. | ||
| */ |
| sizediff_t minIndex(alias pred = "a < b", Range)(Range range) | ||
| if (isForwardRange!Range && !isInfinite!Range && | ||
| is(typeof(binaryFun!pred(range.front, range.front)))) | ||
| { |
There was a problem hiding this comment.
This runs counter to the behavior of most range functions in Phobos. Normally an empty range is rejected outright with an assert rather than given a special value.
I would change this to assert(!range.empty);
There was a problem hiding this comment.
I struggled with this for a bit. We have minElement and minCount, which has no reasonable way to report an empty range, so it just explodes. We then have minPos, which elegantly - :) - sidesteps the issue by returning a potentially empty range. This kind of leaves minIndex on its own. There are other functions that must deal with empty ranges, can you guys enumerate a few?
There was a problem hiding this comment.
There are other functions that must deal with empty ranges, can you guys enumerate a few?
reduce and fold, #4937 replaces enforce for them with asserts
There was a problem hiding this comment.
@9il thx. @JackStouffer other precedents we may draw from?
std/algorithm/searching.d
Outdated
|
|
||
| ptrdiff_t min_pos = 0, cur_pos = 0; | ||
| auto min = range.front; | ||
| Unqual!(typeof(range.front)) min = range.front; |
There was a problem hiding this comment.
which will fail, as min needs to be a Rebindable
| assert(a.maxIndex == 2); | ||
| // Range is empty, so return value is -1 | ||
| assert(b.minIndex == -1); | ||
| // Works for const ranges too |
There was a problem hiding this comment.
The tests should also test more than the default string lambda.
- lambda
- another string lambda
|
@JackStouffer I think we're good to go. BTW there's more precedent: http://dlang.org/phobos/std_string.html#.indexOf. Can you please approve today so we move forward. Thanks! |
|
@JackStouffer ping |
wilzbach
left a comment
There was a problem hiding this comment.
My biggest remark is that we should have a lot more tests! (see comments for details)
It will really help to prevent regressions (and sometimes even fix them ahead-of-time) ;-)
| return range.minIndex!((a, b) => binaryFun!pred(b, a)); | ||
| } | ||
|
|
||
| /// |
There was a problem hiding this comment.
I think you should be to add pure nothrow attributes as well here - this well help to ensure we don't break it in the future.
| assert(a.minIndex!"a > b" == 2); | ||
| // Lambda | ||
| assert(c.minIndex!((a, b) => a < b) == 3); | ||
| } |
There was a problem hiding this comment.
Please add additional tests for
@nogc(withstatic immutable)- different input ranges (have a look in std.range for DummyRange -> e.g. have a look here or here
- test with strings
- Infinite ranges (
static assert) - Immutable ranges
- With common element types (classes, structs, tuples)
std/algorithm/searching.d
Outdated
| assert(a.minIndex == 3); | ||
| // Maximum is 4 and first occurs in position 2 | ||
| assert(a.maxIndex == 2); | ||
| // Range is empty, so return value is -1 |
There was a problem hiding this comment.
maybe it's just me, but I do prefer splitting this into separate unittest blocks - they then will also generated more nice on dlang.org under the "Examples" section & it also helps the reader to keep track of the arrays.
At the very least it would be nice to group them like here:
int[] a = [2, 3, 4, 1, 2, 4, 1, 1, 2];
// Minimum is 1 and first occurs in position 3
assert(a.minIndex == 3);
int b[];
assert(b.minIndex == -1);| } | ||
| return minPos; | ||
| } | ||
|
|
There was a problem hiding this comment.
Unfortunately ditto isn't that smart - it will just copy over the entire documentation string.
I just saw that's the same style as min/maxPos, but to be honest I am a fan of splitting up the docs, s.t. the description, test and returns nicely fit to the according function. A good example and very related example is minElement:
https://dlang.org/phobos/std_algorithm_searching.html#.minElement
| { | ||
| min = range.front; | ||
| minPos = curPos; | ||
| } |
There was a problem hiding this comment.
Please add a special path for arrays - unfortunately it will be a lot faster.
Take a look at extremum
|
@JackStouffer I'll dismiss the review but not in protest :). Feel free to intervene later, we have time til the next release. |
|
Done @andralex . Thank you |
|
@wilzbach Is it ok now? |
| { | ||
| static if (isForwardRange!DummyType && !isInfinite!DummyType) | ||
| { | ||
| DummyType d; |
There was a problem hiding this comment.
you can also define your custom payload, e.g. you could add sth. like:
d.arr = [5, 3, 7, 2, 1, 4];
assert(d.minIndex = 4);
d.arr = [];
assert(d.minIndex = -1);| import std.range: cycle; | ||
| static assert(!__traits(compiles, cycle([1]).minIndex)); | ||
|
|
||
| // with all dummy ranges |
There was a problem hiding this comment.
very minor nitpick: The Phobos style is to use selective imports whenever only a few symbols are needed. For local imports this is nearly always the case ;-)
The motivation is that if the symbols are listed explicitly, it's really easy to see for a reader where the symbols are coming from. We'll hopefully add a check for this soon.
-> import std.internal.test.dummyrange : DummyRange
| // with strings | ||
| assert(["b", "a", "c"].minIndex == 1); | ||
|
|
||
| // infinite range |
There was a problem hiding this comment.
D style is import std.range : cycle;, the motivation here was to have a unified style. This is even enforced by our CircleCi bot, though I just [realized[(https://github.com//pull/4955) that the style fix doesn't propagate to PR builds.
Btw you can run the style checks locally with make -f posix.mak style
std/algorithm/searching.d
Outdated
| assert(b.minIndex == -1); | ||
|
|
||
| const int[] c = [2, 5, 4, 1, 2, 3]; | ||
| // Works for const ranges too |
There was a problem hiding this comment.
Huh - you left one instance of maxIndex here.
Btw in general a a user expects that it works with const. So it doesn't make much sense to add it to the public unittest, but to the internal ones
std/algorithm/searching.d
Outdated
| const int[] c = [2, 5, 4, 1, 2, 3]; | ||
| // Works for const ranges too | ||
| assert(c.maxIndex == 1); | ||
| // Lambda |
There was a problem hiding this comment.
How about using a custom type here - could be a lot less confusing and closer to the real world (to flip the predicate one would use max):
struct Dog { int age; }
auto dogs = [Dog(10), Dog(5), Dog(15)];
assert(dugs.minIndex!`d1.age < d2.age` == 3);Note that not everyone is a friend of string lambda, but imho they are awesomely concise and thus well fitted for short example snippets.
| { | ||
| static if (isForwardRange!DummyType && !isInfinite!DummyType) | ||
| { | ||
| DummyType d; |
There was a problem hiding this comment.
see the comment over at minIndex on passing in a custom payload
| range = The input range to search. | ||
|
|
||
| Returns: | ||
| The index of the first encounter of the minimum element in `range`. If the |
There was a problem hiding this comment.
You are inconsistent here, In the last sentence you don't escape range.
FYI function parameters get automatically escaped in ddoc ;-)
| range = The input range to search. | ||
|
|
||
| Returns: | ||
| The index of the first encounter of the maximum in `range`. If the |
There was a problem hiding this comment.
ditto (inconsistency with range)
std/algorithm/searching.d
Outdated
|
|
||
| Returns: | ||
| The index of the first encounter of the maximum in `range`. If the | ||
| range is empty, -1 is returned. |
There was a problem hiding this comment.
how about:
See_Also:
$(REF max, std,algorithm,comparison), $(LREF maxCount), $(LREF maxElement), $(LREF maxPos)
std/algorithm/searching.d
Outdated
|
|
||
| Returns: | ||
| The index of the first encounter of the minimum element in `range`. If the | ||
| range is empty, -1 is returned. |
There was a problem hiding this comment.
how about
See_Also:
$(REF min, std,algorithm,comparison), $(LREF minCount), $(LREF minElement), $(LREF minPos)
|
@wilzbach Thank you very much for the detailed review. If there is something else that needs changing please tell me. |
wilzbach
left a comment
There was a problem hiding this comment.
The examples should should good practices ;-)
std/algorithm/searching.d
Outdated
|
|
||
| // Works with more custom types | ||
| struct Dog { int age; } | ||
| Dog[] dogs = [Dog(10), Dog(5), Dog(15)]; |
There was a problem hiding this comment.
ehm we want to show the users good practice examples - using maxIndex for the min isn't one -> Please change to sth .like
Dog[] dogs = [Dog(10), Dog(15), Dog(5)];
assert(dogs.maxIndex!"a.age < b.age" == 1);
wilzbach
left a comment
There was a problem hiding this comment.
Thanks a lot for taking the time to address all my comments!
|
You are welcome, @wilzbach . These are my first PRs as a D contributor, so I really need to get in line with the rigors. Thank you for your review. |
Looking forward to your next PR!
@JackStouffer with the new GitHub approve/request feature, you are blocking merging:
|
|
@JackStouffer seems to be away for a few days now, I'll dismiss his review; there is time for us to address whatever issues are left before releasing. |
There seems to be agreement -1 is okay to return
…mix or max item Issue 8573 - A simpler Phobos function that returns the index of the mix or max item added some review fixes fixed an issue with a mutable variable Applied review feedback Renamed functions to minIndex and maxIndex + used sizediff_t for return value type Updated function so that it works optimally even for lazy ranges and algorithms Reverted to having only copyable elements in ranges Added more unittests; implemented an array path; fixed documentation Squashed commits
|
Auto-merge toggled on |
|
@andralex Yeah, sorry. Was busy moving back to the US :P I still think that a contract with an assert would be best, however I no longer see it as deal breaking because of the precedent with |
…mix or max item