On Friday, 14 August 2015 at 00:33:30 UTC, Luís Marques wrote:
Despite using them all the time, I'm suddenly confused about ranges...

My understanding is that (for library-worth code) algorithms that consume ranges are supposed to use .save() to be compatible with both ref type and value type ranges.

I don't think we have that requirement. When you don't want the range to be consumed, you should make the call on a `.save`d copy.

What about the reverse? Are algorithms supposed to try to avoid effectively .save()ing ranges (when they are value types) by not copying range variables?

Algorithms cannot assume that copying is the same as `.save`ing. I think copying is ok, but once you've `.popFront`ed the original or the copy, you can't assume that the other one is still usable.

Furthermore, is it incorrect for an input range that is also not a forward range (or at least does not declare itself formally as a forward range by having a save()) to allow itself to be bitwise copied? (i.e., to effectively provide a save() behaviour).

It's tempting. I don't how practical or enforceable it would be.

To make it a more concrete, consider the following:
[...]
    void main()
    {
        auto vl = ValInputRange();
        auto rf = new RefInputRange();

        assert(vl.startsWith([0, 1]) == true);
        assert(vl.startsWith([0, 1]) == true);

        assert(rf.startsWith([0, 1]) == true);
        assert(rf.startsWith([0, 1]) == false);

        writeln(vl.take(3)); // [0, 1, 2]
        writeln(rf.take(3)); // [1, 2, 3]
    }

- is startsWith() supposed to be transparently usable with both val-type ranges and val-type ranges? Currently it provides different semantics for these, arguably, as in the example.

From another point of view, startsWith does the same on both ranges, and it's the ranges that are behaving differently.

That means, if you don't want startsWith to consume your range, .save it in the call. Otherwise, be prepared for startsWith to .popFront stuff away.

- startsWith accepts an InputRange. What does this mean in practice? I'm used (perhaps incorrectly) to thinking about a proper InputRange (one that isn't also a ForwardRange, etc.) as one that provides a single pass. But many algorithms, such as startsWith, have multi-pass semantics: despite not save()ing the range explicitly, since startsWith() receives the range by value, it is copied, and therefore a subsequent pass is available.

That's not a property of startsWith but of the range. startsWith could force reference semantics with a ref parameter. But then you couldn't pass rvalue ranges, which would be annoying.

- If the current (dual) behaviour is desired, shouldn't startsWith at least document how it mutates the inputed range?

Maybe. But being vague has value in that the exact behavior can change later on without breaking what's documented. If a user can't accept vague popping, .save is there.

[...]

- shouldn't InputRanges be more explicit about their implicit ForwardRange'ness or lack thereof? For instance, is it 100% obvious to everybody which of these versions will be correct? For all acceptable implementations of File and .byLine()?

    void main()
    {
        import std.file : write;
        import std.stdio : File;
        import std.algorithm;

write("/tmp/test.txt", "line one\nline two\nline three");
        auto lines = File("/tmp/test.txt").byLine;

        // guess which...

        version(one)
        {
assert(lines.startsWith(["line one", "line two"]) == true); assert(lines.startsWith(["line one", "line two"]) == true);
        }

        version(two)
        {
assert(lines.startsWith(["line one", "line two"]) == true); assert(lines.startsWith(["line two", "line three"]) == true);
        }
    }

I don't think it's obvious, and I don't think you should rely on the actual behavior if it's not documented. Instead force things your way with .save or std.range.refRange:

When you don't care if a range is popped or not: Pass it as it is.
When you want it to be popped: refRange. Can pass as is when the range has proper reference semantics or when passing via a ref parameter. When you don't want it to be popped: .save. Can pass as is when the range has proper value semantics and it's not passed via a ref parameter.

Applied to the example:
----
version(one)
{
    assert(lines.save.startsWith(["line one", "line two"]));
    assert(lines.save.startsWith(["line one", "line two"]));
}

version(two)
{
    import std.range: refRange;
    assert(refRange(&lines).startsWith(["line one", "line two"]));
assert(refRange(&lines).startsWith(["line two", "line three"]));
}
----

Version one doesn't compile, meaning you can't enforce value semantics.

Version two compiles, but since startsWith is vague about its use of .popFront, you probably shouldn't rely on what's there after the first call.

In the end, byLine and startsWith don't work together well, because the range is a mere input range, and the algorithm is not about popping things. Maybe you should write your own ifStartsWithThenPopIt (with a better name).

tl;dr: use .save and std.range.refRange

Reply via email to