[swift-evolution] [Discussion] stride behavior and a little bit of a call-back to digital numbers
Erica Sadun
erica at ericasadun.com
Sat Feb 27 17:27:37 CST 2016
Following up to myself. Thoughts and feedback are welcome. -- Erica
Changing the Behavior of StrideThroughGenerator
Swift offers two stride functions, stride(to:, by:) and stride(through:, by:). I propose to change the way the through variation works.
Current Art
A Strideable to sequence returns the sequence of values (self, self + stride, self + stride + stride, … last) where last is the last value in
the progression that is less than end.
A Strideable through sequence currently returns the sequence of values (self, self + stride, self + tride + stride, … last) where last is the last value in the progression less than or equal to end. There is no guarantee that end is an element of the sequence.
Under the current implementation, each floating point addition accrues errors. The progression never reaches 2.0.
print(Array(1.0.stride(through: 2.0, by: 0.1)))
/// Prints [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9]
To force the progression to include 2.0, you must add an (ugly) epsilon, as in the following example:
print(Array(1.0.stride(through: 2.01, by: 0.1)))
/// Prints [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
This is problematic for the following reasons:
The name of the calling function “through” suggests the progression will pass through the end point before stopping
Floating point calls present an extremely common use-case
It’s unreasonable to expect developers to consider every case of “will floating point math prevent my progression from actually reaching the end point, which has already been differentiated by using through rather than to”
Proposed Modifications
I recommend the following changes:
Change the documentation text from
A Strideable through sequence currently returns the sequence of values (self, self + stride, self + stride + stride, … last) where last is the last value in the progression less than or equal to end. There is no guarantee that end is an element of the sequence.
to
A Strideable through sequence currently returns the sequence of values (self, self + stride, self + stride + stride, … last) where last is the last value in the progression greater than or equal to end. There is no guarantee that end is an element of the sequence.
Modify the implementation
Current:
/// Advance to the next element and return it, or `nil` if no next
/// element exists.
public mutating func next() -> Element? {
if done {
return nil
}
if stride > 0 ? current >= end : current <= end {
if current == end {
done = true
return current
}
return nil
}
let result = current
current += stride
return result
}
Proposed:
/// Advance to the next element and return it, or `nil` if no next
/// element exists.
public mutating func next() -> Element? {
if done {
return nil
}
if stride > 0 ? current >= end : current <= end {
// NOTE: `current >= end` and not `current == end`
if current >= end {
done = true
return current
}
return nil
}
let result = current
current += stride
return result
}
}
Introduced Changes
Under these changes, the following progression ends at 2.0 not 1.9:
print(Array(1.0.stride(through: 2.0, by: 0.1)))
// prints [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
Integer progressions are unchanged:
print(Array(1.stride(through2: 10, by: 1)))
/// prints [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Floating point strides will extend up-to or past the through value:
// Old
print(Array(1.0.stride(through: 1.9, by: 0.25)))
// prints [1.0, 1.25, 1.5, 1.75]
// New
print(Array(1.0.stride(through: 1.9, by: 0.25)))
// prints [1.0, 1.25, 1.5, 1.75, 2.0]
Alternates Considered
Other changes could include:
Introducing a digitalStride function with a set precision that works in integer math, multiplying each value by 10n, converting to integers, and then working back to floating point after each change
Counting expected iterations by forming (max - min) / by, e.g. (2.0 - 1.0) / 0.1, which is 10, and performing each step as a pro-rated progression along those steps, which would remove most of the accumulated floating point errors along the way.
Introducing a DecimalNumber type, with its own stride methods, e.g. DecimalNumber(1.0).stride(through:DecimalNumber(2.0), by: DecimalNumber(0.1)).
> On Feb 26, 2016, at 5:12 PM, Erica Sadun via swift-evolution <swift-evolution at swift.org> wrote:
>
> I have a problem with the way floating point ranges work with striding:
> 1.0.stride(through: 2.0, by: 0.1) returns the sequence [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9].
> Documentation: "It returns the sequence where last is less than or equal to `end`."
> (And yes, the same issue exists with tradition C-style for loops).
>
> Would it be really horrible if the implementation and definition was changed to:
> "It returns the sequence where last is greater than or equal to `end`?"
> This would offer no change for integers, and include 2.0 for floating point sequences.
>
> Alternatively, could there be decimalStride? Using Double but a rounding system with a fixed number of decimal places (e.g. 1, 2, 3), to ensure at least the end point is hit? It might look like:
> 1.0.stride(through: 2.0, by: 0.1, places: 1)
>
> I know there have been several discussions on-list about decimal number systems (Re: Is there a need for a Decimal type? <http://article.gmane.org/gmane.comp.lang.swift.evolution/7130/match=decimal>) as well, but this could fix an ongoing annoyance without a major change.
>
> Thanks for your thoughts,
>
> -- Erica
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160227/ab94455c/attachment.html>
More information about the swift-evolution
mailing list