Skip to content

Add std.range.tail #3855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 12, 2016
Merged

Add std.range.tail #3855

merged 1 commit into from
Jan 12, 2016

Conversation

JakobOvrum
Copy link
Member

Forum thread:
http://forum.dlang.org/post/n3sfgg$1hpm$3@digitalmars.com

Differences from Andrei's suggestion:

  • Returns Range, not TakeExactly!Range - the end of the resulting range is always the same as the input so this checks out and is a useful artifact.
  • Supports non-forward ranges when hasLength.
  • Rejects infinite ranges.

I thought tail was clever so I named it that, but I'll happily change it to the originally proposed advanceWithin if people want that.

@JakobOvrum JakobOvrum force-pushed the range_tail branch 5 times, most recently from 780bdc5 to d856f3a Compare December 5, 2015 17:41
{
static immutable input = [1, 2, 3];
static immutable expectedOutput = [2, 3];
assert(input.tail(2) == expectedOutput);
Copy link
Member

Choose a reason for hiding this comment

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

static assert?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's for checking @nogc, so no

Copy link
Member

Choose a reason for hiding this comment

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

You can do both at the same time, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean?

Copy link
Member

Choose a reason for hiding this comment

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

If you change it to a static assert, you can test CTFE and @nogc at the same time, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Compile-time evaluated expressions are not restricted by the attributes of the surrounding function.

When it comes to testing CTFE, this isn't generally done for most of our library. To wit, the only function in std.range.package that tests CTFE appears to be takeNone. This isn't entirely unreasonable because CTFE restrictions aren't really relevant in high level code.

However, issues do crop up when optimizations are applied, such as for array and find. In this case it's obvious that no CTFE restrictions are violated, but perhaps in some strange future this algorithm too could be optimized with some kind of low level trick. When testing CTFE though, it's not enough to test just one application - not only will different input types produce different functions, but CTFE restrictions only apply when they are encountered in the path currently being executed.

In this particular case CTFE can be tested decently well by just adding a single line to the static foreach, so I did that. It's not that simple in general.

@JakobOvrum
Copy link
Member Author

@MetaLang, I missed your comment, but the answer is probably: checking both for empty and comparing length with n is probably more work than necessary in most cases. It is assuming that empty is not significantly cheaper than length. Also, most calls are probably on non-empty ranges? BTW, I've found that deleting a comment can really confuse email users!

@MetaLang
Copy link
Member

MetaLang commented Dec 5, 2015

Yes, I realized this just after I posted the comment so I deleted it.

@JackStouffer
Copy link
Member

LGTM.

I like tail better than advanceWithin; instantly makes sense to posix users.

@andralex
Copy link
Member

andralex commented Dec 6, 2015

The idea behind TakeExactly is to provide extra information to the user. The length of the result is already computed so it's more informative to make it part of the result. Otherwise that information is lost.

}
else
{
if (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.

No need for this - just move scout.popFront() to the end of the loop below.

Copy link
Member Author

Choose a reason for hiding this comment

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

It means more calls to save when the length of range is 0 or n + 1, but I suppose there's no reason to optimize for these cases.

@JakobOvrum
Copy link
Member Author

The current implementation is cute, but is it any different from the following?

immutable length = range.walkLength; // note: O(1) when range has length
auto tail = range.drop(n > length? length - n : 0); // note: O(1) when range has slicing

@JakobOvrum JakobOvrum force-pushed the range_tail branch 2 times, most recently from 673857a to 76cb935 Compare December 7, 2015 20:09
}

auto tail = range.save;
while(!scout.empty)
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: space between while and (.

@quickfur
Copy link
Member

quickfur commented Dec 9, 2015

Other than that, LGTM.

Suggested by Andrei Alexandrescu in this thread:
http://forum.dlang.org/post/n3sfgg$1hpm$3@digitalmars.com
@JakobOvrum
Copy link
Member Author

Fixed.

Reimplementing this in user code requires at least one variable which makes it tedious for range composition, so even if the implementation offers no significant advantage in terms of performance or range capabilities (both input and output), I think it's worth including. Further, I think the branch for !hasLength!Range in the current implementation might offer some marginal improvement in edge cases (for one it's not limited by size_t unlike drop) and it's nice and short, so I think it's probably fine as-is.

.lineSplitter
.tail(2)
.joiner("\n")
.equal("two\nthree"));
Copy link
Member Author

Choose a reason for hiding this comment

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

Just now in a script I could have used just this pattern (lineSplitter + tail) to parse some output from a child process. Neat.

@JakobOvrum
Copy link
Member Author

I think I should add a path for bidirectional ranges with length.

What about bidirectional ranges without length, though? Should we assume the beginning of the tail is closer to the end than the beginning?

@JackStouffer
Copy link
Member

Should we assume the beginning of the tail is closer to the end than the beginning?

I'd say that that's a pretty safe assumption.

@JakobOvrum
Copy link
Member Author

When n is more or less constant, the beginning of the tail moves proportionally closer to the end of the range as the number of elements in the range grows. That's pretty good for iterating from the back.

If n is calculated with the length of the range, we can experience the opposite, which is not good. However, this is probably a mistaken use of tail - stuff like drop should be used instead.

When n is not constant but calculated unrelated to the length of the range, then it cannot be known whether iterating from the front or iterating from the back is better.

So I guess iterating from the back is the slightly better choice overall.

@JakobOvrum
Copy link
Member Author

Erm, iterating from the back is not readily possible. Not sure what I was thinking.

@JackStouffer
Copy link
Member

Erm, iterating from the back is not readily possible. Not sure what I was thinking.

If you add the ability to make n a template argument you could.

@JakobOvrum
Copy link
Member Author

How so?

@JackStouffer
Copy link
Member

You could make a static array as a buffer ElementType!Range[n] buff for what ever you get off the end.

@JakobOvrum
Copy link
Member Author

E[n] buffer;
auto remainder = range.retro.take(n).copy(buffer[].retro);
auto result = buffer[remainder.length .. $];

Whether or not this is appropriate for a standard library function aside, I think it's inappropriate for tail for a number of reasons, primarily because the return type would have to work like only and carry all its elements in-place, which is fundamentally different from what tail does now.

@JackStouffer
Copy link
Member

@quickfur @andralex This looks good to go from people's comments.

@quickfur
Copy link
Member

quickfur commented Jan 6, 2016

ping @andralex

Is this OK to merge? Are the objections you raised about using TakeExactly still blocking this PR, or the explanation is good enough?

@JakobOvrum
Copy link
Member Author

Oh, I forgot to mention that the implementation does use takeExactly now. I think there are benefits to either way.

I haven't linked this to an issue report yet. (edit: or would a manual changelog entry be preferable?)

@JackStouffer
Copy link
Member

would a manual changelog entry be preferable?

I think so

@andralex
Copy link
Member

Auto-merge toggled on

andralex added a commit that referenced this pull request Jan 12, 2016
@andralex andralex merged commit 1e74490 into dlang:master Jan 12, 2016
@nordlow
Copy link
Contributor

nordlow commented Jan 13, 2016

For symmetry I truly feel that front() now should have an extra optional size_t n argument aswell. What do you say?

@JakobOvrum
Copy link
Member Author

@nordlow, what do you mean? The front version of tail(n) is take(n).

@andralex
Copy link
Member

@nordlow that won't work. Some ranges define front property-style, with a getter and setter overloads.

@nordlow
Copy link
Contributor

nordlow commented Jan 13, 2016

@andralex Ok.

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.

7 participants