On Mon, Dec 23, 2013 at 03:00:25PM +0000, Francesco Cattoglio wrote:
> Sorry for the amount of text, but I tried to explain everything in a
> clear and simple way.
> I'm willing to volunteer for adding support for more types in iota.
> The relevant discussion is:
> https://d.puremagic.com/issues/show_bug.cgi?id=10762
> 
> A few preliminary considerations:
> - iota() currently returns a Random Access range, and I'm sure
> nobody would agree to downgrade the result to a ForwardRange or an
> InputRange, because that would break an unknown amount of code.

Certainly, the new iota() implementation should not reduce current
functionality, but only add to it.


> - I think everyone agrees that even after enhancements, iota()
> should always return a RA range, even if we use types that are
> currently not supported. It would make little sense returning
> different kind of ranges for different input types.

This is false. Phobos has a lot of code that returns slightly different
types depending on what the input was. For example, .map returns a
random access range if the input is random access, a bidirectional /
forward / input range if that's that the input is, etc.. This is
perfectly normal.

It makes little sense to require, e.g., .map to always return, say, a
forward range even if you hand it an input range -- it wouldn't be able
to do this without introducing very inefficient code (like caching
entries so that it can implement .save). Phobos range adaptors should
always return the "best of input functionality", i.e., if the incoming
range has .save, then where reasonable the resulting range also should
have .save; ditto for opIndex, .back, .popBack, etc..


> Let's define the types: When calling iota(start,end,inc)
>   S=typeof(start),
>   E=typeof(end),
>   I=typeof(inc),
>   C=CommonType!(S,E).
> 
> My proposal:
> iota(start, end, inc) accepts any type, as long as
>     {C c; c += inc; c < end;} or {C c; c = c + inc; c < end;} is
> valid code;
>
> Since inc type might not be directly comparable to 0, we also check
> if (start + inc < start). If this returns true, this means that "inc
> is negative" in a more general sense.

I'm a bit uneasy about this, but I guess it's the price of completely
generic code, since you're right, there's no guarantee the inc type is
comparable to 0.


> We can use this information to return an empty range when needed,
> respecting the behaviour defined by the library reference. eg: (start
> < end && inc < 0) => return
> empty range.
>
> end is not required to be equal to (start + n*inc) for any given n;
> eg: iota(4, 9, 2) should be valid and return [4, 6, 8]. We can
> discuss if this makes sense or should be changed, but I think it
> would be better this way.

I think this way is better.


> If the code {int n; I n_inc = n * inc;} is valid, this instruction
> is used for providing O(1) access for opIndex and opSlice methods.

Yes.


> If the code {C c; c -= inc;} or {C c; c = c - inc} is valid, this
> instruction is used for providing O(1) access for popBack() method.

Yes.


> If no optimization is available, the code will provide O(n)
> performance for those methods.

No. If (start + n*inc) is not compilable, then iota should not be a
random-access range. I don't like the idea that iota[n] is O(1) for some
types and O(n) for other types.


> The real issue however is that construction of the range might end up
> taking O(n) time, because we have to provide the length and the back
> property. One work around is computing those lazily only if the
> user requests them.

No, if (start + n*inc) is not available, don't provide .length and don't
provide .back. Otherwise you're adding unnecessary baggage in the
returned range type for most code that don't care about .length or
.back.


> By doing this, anyone only using iota() for forward iteration will
> still obtain O(1) performance.
> 
> iota(start, end) accepts any type, as long as
>     {C c; ++c; c < end;} or {iota(start, end, 1);} is valid code. If
> both forms are valid, the iota(start, end, 1) version is preferred
> (allowing possible optimizations as discussed before).
> Everything else ends up being the same as before: popBack() can be
> optimized if {--c;} is valid code, the opIndex, opSlice, back and
> length methods will take O(n) time, no way around this sadly.

No, we should not implement any O(n) time methods in iota. If only ++ is
available in the base type, then .length, .back, opSlice, will not be
available. Basically, if the user type only supports ++, then in normal
user code they will not expect to compute length easily, or to be able
jump to over several consecutive values. So chances are that they won't
be using them with the range returned by iota either. So this will just
be unneeded added complexity in the iota implementation. I vote against
it.


> hsteoh suggested:
>     If start+inc is *not* supported but start-- is, and end < start,
> should we support iota(start,end)?
> I think this idea should be discarded since some code could quickly
> become a minefield:
> type T implements ++t and --t;
> type P only implements ++p;
> t1 < t2 => iota(t2, t1) has a way to compute a non-empty range;
> p1 < p2 => iota(p2, p1) can do nothing but return an empty range;
> And this behaviour really smells bad, IMHO. The only way to solve
> this is requiring opUnary!"--" being implemented for the type to be
> used, and I am not sure about this (most iota uses only really need
> ++).

I think this part is optional, I won't miss it if it won't be not there,
actually. I only suggested it because I thought it would be nice to
support iota(a,b,-1) for types that don't have a way to specify inc. But
like you said, it's probably very rare for people to actually want to do
this.


> Any suggestion/critique? If this looks good to you, I can start
> working on it. If you disagree on something (and I know you have
> something to say!), discussion is more than welcome!
[...]

I think overall it looks good, except for the parts where you try to
implement .back, .length, opSlice, etc., when the underlying type
doesn't support it. I think that's unnecessary work. Just as we don't
expect sqrt() to return a meaningful value for -1 for real numbers, but
we *do* expect a meaningful value for complex numbers, so there's no
reason iota() must always return a RA range. If the user wants RA
access, then he should implement more primitives in his type, it's not
iota's job to do that for him.

Another thing to be mindful of is the current existing overloads of
iota. Your implementation should either be in distinct overloads from
them (so that they continue to work as they do now), or it should
subsume them and provide (at least) equivalent functionality, if not
better. In no case should existing functionality be broken by the new
replacement.

This sounds obvious but may be harder than it appears, because there's
an overload (or multiple overloads?) specifically for floating-point
types, and you'll have to take into account the quirks of floating-point
types to handle all the cases correctly. Keep in mind that if a float is
too large, iota() may not be able to return a meaningful range (e.g.,
x+inc may not have a distinct floating-point representation from x when
abs(x) is too big relative to inc). Also, what should be done if
mathematically x + n*inc == end exactly, but due to roundoff error it
misses by a few ulps.  Then iota() may have different lengths depending
on CPU (e.g. 80-bit real vs. 128-bit real), rounding flags, etc., and
things could get hairy. You might want to keep the floating-point
overload(s) of iota as-is to avoid having to deal with this mess. :)


T

-- 
One disk to rule them all, One disk to find them. One disk to bring them all 
and in the darkness grind them. In the Land of Redmond where the shadows lie. 
-- The Silicon Valley Tarot

Reply via email to