Skip to content

Issue 8573 - A simpler Phobos function that returns the index of the …#4921

Merged
wilzbach merged 1 commit intodlang:masterfrom
RazvanN7:Issue_8573
Dec 16, 2016
Merged

Issue 8573 - A simpler Phobos function that returns the index of the …#4921
wilzbach merged 1 commit intodlang:masterfrom
RazvanN7:Issue_8573

Conversation

@RazvanN7
Copy link
Collaborator

…mix or max item

@dlang-bot
Copy link
Contributor

Fix Bugzilla Description
8573 A simpler Phobos function that returns the index of the mix or max item

@ntrel
Copy link
Contributor

ntrel commented Nov 25, 2016

  • getMinPos vs existing minPos is not descriptive. Maybe call it minPosIndex.
  • The two versions above should share a single implementation.

There doesn't seem to be a version of find that returns an index. It seems that might be a necessary precedent before adding this PR. There is std.string.indexOf, which is string-specific.


Returns:
The position of the first encounter of the minimum (respectively maximum) in `range`.
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should return a ptrdiff_t.

return min_pos;
}

///Ditto
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as previously, return a ptrdiff_t

@ntrel
Copy link
Contributor

ntrel commented Nov 26, 2016

I added an alternate idea using std.range.enumerate to the bug issue.

assert(minPos!("a[0] < b[0]")(b) == [ [2, 4], [4], [4] ]);
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index of the first occurrence...

maximum) element.
range = The input range to search.

Returns:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be ptrdiff_t

auto min = range.front();
for (range.popFront(); !range.empty; range.popFront())
{
++cur_pos;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

front should not have () parentheses - ditto for min initialisation.
It might be better to avoid copying range.front here.

@ntrel
Copy link
Contributor

ntrel commented Nov 28, 2016

Using mutable min temporary means this won't work when front is const.

@ntrel
Copy link
Contributor

ntrel commented Nov 28, 2016

Re: my original comment about precedent with find - Andrei added the bootcamp keyword to the Issue previously, so he approves of this.

Copy link
Member

@burner burner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The given range should be a forward range otherwise the index is useless as there is no more range to index.

Returns:
The position of the first encounter of the minimum (respectively maximum) in `range`.
*/
int getMinPos(alias pred = "a < b", Range)(Range range)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InputRange is a struct so it is passed by value. The function does not consume the range from the caller context.

@wilzbach
Copy link
Contributor

Do we really need a new function for this?
Since 2.072 we can use this handy syntax

[2, 4, 1].enumerate.maxElement!`a.value`.index; // 1

or even shorter with the array syntax:

[2, 4, 1].enumerate.maxElement!`a[1]`[0]; // 1

Heck we can even use maxPos:

[2, 4, 1].enumerate.maxPos!`a.value`[0].index - 1; // 1

I 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))))
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're good here.


ptrdiff_t min_pos = 0, cur_pos = 0;
auto min = range.front;
Unqual!(typeof(range.front)) min = range.front;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a unittest for const front.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which will fail, as min needs to be a Rebindable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@burner: Yes, Rebindable should be made a generic type constructor, not just for reference types. The main change needed for that is to get #4363 in good shape and merged, the other value types should be trivial.

@andralex
Copy link
Member

andralex commented Dec 6, 2016

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.

Copy link
Member

@andralex andralex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return ssize_t instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's not quite what you want. ssize_t is used in druntime. I guess we can use Signed!size_t.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, got it. Thanks

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

range is empty, -1 is returned.
*/
ptrdiff_t minPosIndex(alias pred = "a < b", Range)(Range range)
if (isInputRange!Range && !isInfinite!Range &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))))
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're good here.

{
if (range.empty) return -1;

ptrdiff_t min_pos = 0, cur_pos = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use underscores in variable names, only in package and module names. Use thisConvention.

Unqual!(typeof(range.front)) min = range.front;
for (range.popFront(); !range.empty; range.popFront())
{
++cur_pos;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after if. @wilzbach we really should catch this automatically :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do, but our automatic style checking got disabled this autumn. I am playing catching up since a while now -> #4850

Returns:
The index of the first encounter of the minimum (respectively maximum) in `range`. If the
range is empty, -1 is returned.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizediff_t

is(typeof(binaryFun!pred(range.front, range.front)) == bool))
{
if (range.empty) return -1;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizediff_t

{
if (range.empty) return -1;

Signed!size_t minPos = 0, curPos = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not to store the element instead of range?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporary is better here. Just use Unqual!(...) for min declaration

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is lazy ranges and lazy algorithms. front should be called only once if possible

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my initial solution, but @andralex told me that not all types are copyable to their unqualified type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The common use case is auto amax = range.map!abs.maxIndex;. OK lets use .front

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a static if which only copies 1 element when the elements are copyable. Is that ok on your behalf?

return minPos;
}

///Ditto
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizediff_t

Returns:
The index of the first encounter of the minimum (respectively maximum) in `range`. If the
range is empty, -1 is returned.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minPosIndex -> minIndex

@andralex
Copy link
Member

andralex commented Dec 8, 2016

Thx! I see you've also address @burner 's and @9il 's reviews, could you guys approve?

sizediff_t minIndex(alias pred = "a < b", Range)(Range range)
if (isForwardRange!Range && !isInfinite!Range &&
is(typeof(binaryFun!pred(range.front, range.front))))
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say we can return -1 here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

@9il 9il Dec 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@9il thx. @JackStouffer other precedents we may draw from?


ptrdiff_t min_pos = 0, cur_pos = 0;
auto min = range.front;
Unqual!(typeof(range.front)) min = range.front;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests should also test more than the default string lambda.

  • lambda
  • another string lambda

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, thank you

@andralex
Copy link
Member

@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!

@andralex
Copy link
Member

@JackStouffer ping

Copy link
Contributor

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
}

///
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
}
Copy link
Contributor

@wilzbach wilzbach Dec 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add additional tests for

  1. @nogc (with static immutable)
  2. different input ranges (have a look in std.range for DummyRange -> e.g. have a look here or here
  3. test with strings
  4. Infinite ranges (static assert)
  5. Immutable ranges
  6. With common element types (classes, structs, tuples)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}

Copy link
Contributor

@wilzbach wilzbach Dec 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a special path for arrays - unfortunately it will be a lot faster.
Take a look at extremum

@andralex
Copy link
Member

@JackStouffer I'll dismiss the review but not in protest :). Feel free to intervene later, we have time til the next release.

@andralex
Copy link
Member

oh I see @wilzbach has a review as well - @RazvanN7 could you please take a look

@RazvanN7
Copy link
Collaborator Author

Done @andralex . Thank you

@RazvanN7
Copy link
Collaborator Author

@wilzbach Is it ok now?

Copy link
Contributor

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - it looks a lot better now!
I took another pass ;-)

{
static if (isForwardRange!DummyType && !isInfinite!DummyType)
{
DummyType d;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

assert(b.minIndex == -1);

const int[] c = [2, 5, 4, 1, 2, 3];
// Works for const ranges too
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

const int[] c = [2, 5, 4, 1, 2, 3];
// Works for const ranges too
assert(c.maxIndex == 1);
// Lambda
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto (inconsistency with range)


Returns:
The index of the first encounter of the maximum in `range`. If the
range is empty, -1 is returned.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about:

See_Also:
    $(REF max, std,algorithm,comparison), $(LREF maxCount), $(LREF maxElement), $(LREF maxPos)


Returns:
The index of the first encounter of the minimum element in `range`. If the
range is empty, -1 is returned.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about

See_Also:
    $(REF min, std,algorithm,comparison), $(LREF minCount), $(LREF minElement), $(LREF minPos)

@RazvanN7
Copy link
Collaborator Author

@wilzbach Thank you very much for the detailed review. If there is something else that needs changing please tell me.

Copy link
Contributor

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples should should good practices ;-)


// Works with more custom types
struct Dog { int age; }
Dog[] dogs = [Dog(10), Dog(5), Dog(15)];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved. Thanks

Copy link
Contributor

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for taking the time to address all my comments!

@RazvanN7
Copy link
Collaborator Author

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.

@wilzbach
Copy link
Contributor

@wilzbach These are my first PRs as a D contributor, so I really need to get in line with the rigors

Looking forward to your next PR!
I think this looks quite nice now, so:

  • would you be so kind to squash all changes into one commit? (we might support to do that automatically in the near future)
  • open another PR against the Changelog (as the changelog changes quite often, one quickly runs into merge conflicts. Hence atm the least pain approach is to do so with a separate PR. However, we will move to single changelog files soon).

@JackStouffer with the new GitHub approve/request feature, you are blocking merging:

Merging can be performed automatically once the requested changes are addressed.

@andralex
Copy link
Member

@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.

@andralex andralex dismissed JackStouffer’s stale review December 15, 2016 15:20

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
@RazvanN7
Copy link
Collaborator Author

@wilzbach I squashed the commits and made another PR with the changelog updated accordingly; see #4960.

@wilzbach
Copy link
Contributor

Auto-merge toggled on

@wilzbach wilzbach merged commit f3a8401 into dlang:master Dec 16, 2016
@JackStouffer
Copy link
Contributor

@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 indexOf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants