[swift-evolution] Feature proposal: Range operator with step

Dave Abrahams dabrahams at apple.com
Thu Apr 7 13:02:25 CDT 2016


on Thu Apr 07 2016, Brent Royal-Gordon <brent-AT-architechies.com> wrote:

>>> calendar.using(.Day).stride(from: startDate, to: endDate, by: 1)
>> 
>>> The `start` and `end` parameters could be grouped together into a
>>> single parameter to match `stride(over:by:)`, but you can't put the
>>> calendar or the unit into the stride—without them, there is no
>
>>> coherent way to calculate the distance between two dates.
>>> 
>>> So if some types need a strider, and will need to have the method
>>> structured as `strider.stride(something:by:)`, it seems like the
> free
>>> function version for types which *don't* need a strider ought to be
>>> `stride(something:by:)`. The `something.striding(by:)` design can't
> be
>>> easily adapted to this situation.
>> 
>>  calendar[startDate..<endDate].striding(by: .Day)
>> 
>> ?
>
> Actually, it would need to be something like
> `calendar[startDate..<endDate, unit: .Day].striding(by: 1)`, because
> NSCalendarUnit is not itself a stride, it is the *unit* of the
> stride. 

Maybe:

  calendar.days[startDate..<endDate].striding(by: 1)

> But even that doesn't quite help.
>
> Here's the issue.
>
> Take a look at today's Strideable. Stripped of comments and irrelevant
> annotations, you have:
>
> 	protocol Strideable : Comparable {
> 	    associatedtype Stride : SignedNumberType
> 	    func distanceTo(other: Self) -> Stride
> 	    func advancedBy(n: Stride) -> Self
> 	}
>
> The problem is that there's no good way to get the calendar and unit
> into these methods. If you package them inside `Stride`, then
> `distanceTo` has no idea what calendar or unit it's supposed to
> return. If you package them inside `Self`, then `distanceTo` has two
> calendars and two units, and they might not agree (and the type system
> can't catch this issue).
>
> To fix this, you need to have a single instance which performs the
> calculation. For simple Strideable types, this instance would not do
> very much, but for dates, it would factor in the calendar and unit.

I follow all the above; it's a good explanation of the problem.
However, it doesn't explain why

  calendar[startDate..<endDate, unit: .Day].striding(by: 1)

“doesn't quite help.”  It seems to me that 

  calendar[startDate..<endDate, unit: .Day]

does factor in the calendar and unit.

> For example, suppose you modify `Strideable` so that, instead of
> applying to the values participating in the striding, it applies to
> the instance containing those values:
>
> 	public protocol Strideable {
> 		associatedtype Element: Comparable
> 		associatedtype Stride: SignedNumber
>
> 		var start: Value
> 		var end: Value
>
> 		public func distance(from earlier: Element, to later: Element) -> Stride
> 		public func advance(element: Element, by stride: Stride) -> Element
> 	}

Presumably you mean for Strideable to have a striding(by:_) method as well?

If so, how is this fundamentally different from Collection?  Shouldn't
every Collection support this?

> Now, with some *really* aggressive conditional conformance work, you
> could do this:
>
> 	extension Range: Strideable where Element == Int {
> 		typealias Stride = Int
>
> 		public func distance(from earlier: Int, to later: Int)
> -> Int {
> 			return later - earlier
> 		}
> 		public func advance(element: Int, by stride: Int) {
> 			return value + stride
> 		}
> 	}
> 	extension Range: Strideable where Element == Double {
> 		// Ignoring the accumulation issue.
> 		typealias Stride = Double
>
> 		public func distance(from earlier: Int, to later: Int) -> Int {
> 			return later - earlier
> 		}
> 		public func advance(element: Double, by stride: Double) {
> 			return element + stride
> 		}
> 	}

Except we don't have that capability today.  Instead we'd be using
overloads of ..< and ... to produce more-capable range types, c.f.  
https://github.com/apple/swift/blob/swift-3-indexing-model/stdlib/public/core/Range.swift#L504
https://github.com/apple/swift/blob/swift-3-indexing-model/stdlib/public/core/Range.swift#L519

> 	
> 	// etc., for each type you care about.
> 	// 
> 	// This could be automated by creating a `RangeStrideable` protocol similar to the old Strideable,
> 	// and having types with this automatic behavior conform to it. You could then have a single:
> 	// extension Range: Strideable where Element: RangeStrideable
>
> It would not be possible to stride directly over a range of NSDates,
> but you could have a "calendar range" which could be `Strideable`,
> like so:
>
> 	struct NSCalendarRange {
> 		var start: NSDate
> 		var end: NSDate
>
> 		var calendar: NSCalendar
> 		var unit: NSCalendarUnit
>
> 		var options: NSCalendarOptions
> 		// This mainly contains rounding options. Interestingly, NSDate is like Double in that
> 		// it accumulates errors through repeated addition. To fix that, I have discovered
> 		// a truly marvelous design which this email is too small to contain.
> 	}
> 	extension NSCalendarRange: Strideable {
> 		typealias Value = NSDate
> 		typealias Stride = Int
>
> 		public func distance(from earlier: NSDate, to later: NSDate) -> Int {
> 			let components = calendar.components(unit, from: earlier, to: later, options: options)
> 			return components.value(forComponent: unit)
> 		}
> 		public func advance(value: NSDate, by stride: Int) -> NSDate {
> 			return calendar.date(byAdding: unit, value: distance, to: value, options: options)!
> 		}
> 	}
>
> So I guess `striding(by:)` can be adapted to date arithmetic, but only
> if we adjust our conception of what a stride is striding over.


-- 
Dave


More information about the swift-evolution mailing list