Stride Operators, (a..<b)/step and (a...b)/step

I propose use of stride operators to easily create StrideTo and StrideThrough instances.

The definition of stride operators:

For example,

(-10..<10)/4
is equivalent to
stride(from: -10, to: 10, by: 4)

Array((-10..<10)/4)
gives
[-10, -6, -2, 2, 6]

(-10...10)/4
is equivalent to
stride(from: -10, through: 10, by: 4)

Array((-10...10)/4)
gives
[-10, -6, -2, 2, 6, 10]

3 Likes

Cool. Adding a stride operator of some sort is interesting. The / operator's precedence is too tight though. Also, I see what you're going for by re-using /, but it seems like striding is a bit different. Given that strides work on all sequences (not just numeric sequences) so it could be misleading. I'd suggest exploring the idea of using a new operator, dedicated to striding, with its own unique spelling.

-Chris

3 Likes

How about this:

precedencegroup StridePrecedenceGroup {
    lowerThan: RangeFormationPrecedence
}


infix operator ++ : StridePrecedenceGroup
infix operator -- : StridePrecedenceGroup

// Open Range Stride
func ++ <T: Strideable>(left: Range<T>, right: T.Stride) -> StrideTo<T> {
    return stride(from: left.lowerBound, to: left.upperBound, by: right)
}

// Closed Range Stride
func ++ <T: Strideable>(left: ClosedRange<T>, right: T.Stride) -> StrideThrough<T> {
    return stride(from: left.lowerBound, through: left.upperBound, by: right)
}

// Open Range Stride
func -- <T: Strideable>(left: Range<T>, right: T.Stride) -> StrideTo<T> {
    return stride(from: left.upperBound, to: left.lowerBound, by: -right)
}

// Closed Range Stride
func -- <T: Strideable>(left: ClosedRange<T>, right: T.Stride) -> StrideThrough<T> {
    return stride(from: left.upperBound, through: left.lowerBound, by: -right)
}

Array(-10 ... 10 ++ 4) // [-10, -6, -2, 2, 6, 10]
Array(-10 ... 10 -- 3) // [10, 7, 4, 1, -2, -5, -8]

Back with a vengeance :slight_smile:

Edit: the -- is included mostly for symmetry, the logic is completely broken and doesn't respect the boundaries of the ranges. Will be worth thinking about if both a stride operator and a decreasing range make it to the language.

2 Likes

Cute. :smile: Is there a need for two stride operators though?

I don't think there is a need for both today.

I worry about deriving strides from ranges. It would preclude using whatever Range operator is agreed upon to use a negative step to stride from an upper bound to (as opposed to through) a lower bound, as half-open ranges closed at their upper bound don't exist. While Swift doesn't have bottom-open-top-closed ranges, it is perfectly symmetrical regarding whether StrideTo may omit the larger or smaller of its endpoints -- it always omits the destination endpoint. I don't think the asymmetry of Range should carry over to StrideTo.

Perhaps a different syntax altogether might make sense? Something like from..by..<to and from..by...through. (Surely this has been discussed before...?)

Good Suggestion!
I confirmed that (from..by)..<to and (from..by)...through can be implemented as follows:

struct FromByDescriptor<T: Strideable> {
    var from: T
    var by: T.Stride
}

infix operator ..

func .. <T: Strideable>(left: T, right: T.Stride) -> FromByDescriptor<T> {
    return FromByDescriptor(from: left, by: right)
}

func ..< <T: Strideable>(left: FromByDescriptor<T>, right: T) -> StrideTo<T> {
    return stride(from: left.from, to: right, by: left.by)
}

func ... <T: Strideable>(left: FromByDescriptor<T>, right: T) -> StrideThrough<T> {
    return stride(from: left.from, through: right, by: left.by)
}

I agree that starting from a range is not ideal. But I'm also not sure that a..b..<c is better than stride(from: a, to: c, by: b). This is especially the case with 10 .. -4 ..< -10, which doesn't strike me as intuitive at all.

I think a better solution is to add the other handedness of half open range. -10 <.. 10 -- 4 is also not great, but at least the comparison operator is the right way round.

In an ideal world, ranges wouldn't be iterable at all, but that's a whole other discussion.

By the way, Swift has the ternary conditional operator (a ? b : c). Why does Swift not have ternary stride operators (e.g. from .. by ..< to and from .. by ... through)?

I understood the implementation of the ternary stride operators needs a new precedencegroup as the following:

precedencegroup StridePrecedence {
    higherThan: RangeFormationPrecedence
}

infix operator ..: StridePrecedence

Is that correct?

For instance, the implementation for from ..< to .. by and from ... through .. by is as follows:

struct UpperBoundStepDescriptor<T: Strideable> {
    var upperBound: T
    var step: T.Stride
}

precedencegroup StridePrecedence {
    higherThan: RangeFormationPrecedence
    lowerThan: AdditionPrecedence
}

infix operator ..: StridePrecedence

func .. <T: Strideable>(left: T, right: T.Stride) -> UpperBoundStepDescriptor<T> {
    return UpperBoundStepDescriptor(upperBound: left, step: right)
}

func ..< <T: Strideable>(left: T, right: UpperBoundStepDescriptor<T>) -> StrideTo<T> {
    return stride(from: left, to: right.upperBound, by: right.step)
}

func ... <T: Strideable>(left: T, right: UpperBoundStepDescriptor<T>) -> StrideThrough<T> {
    return stride(from: left, through: right.upperBound, by: right.step)
}

Swift has reversed(). So, (-6 .. 4 ... 10).reversed() or (-6 ... 10 .. 4).reversed() can replace 10 .. -4 ..< -10 or -10 <.. 10 -- 4.

Edit: I am sorry. We don't need reversed() for stride:
Array(10 .. -4 ... -9) // [10, 6, 2, -2, -6]
or
Array(10 ... -9 .. -4) // [10, 6, 2, -2, -6]

For stride, ..! might replace ..<.

infix operator ..!: RangeFormationPrecedence

func ..! <T: Strideable>(left: T, right: UpperBoundStepDescriptor<T>) -> StrideTo<T> {
    return stride(from: left, to: right.upperBound, by: right.step)
}

For example,

Array(10 ... -9 .. -4) // [10, 6, 2, -2, -6]
Array(10 ..! -10 .. -4) // [10, 6, 2, -2, -6]

I'm really not a fan of the ternary operator, and I wish it wasn't in the language.

When deciding on an approach for stride, I think that we need something that is easy to understand the first time you see it, and not too cluttered.

I think that what we actually want is a new Stride type with no bounds, one that can be converted to one with bounds with an operator or a method.

struct StrideIterator<Element : Strideable> {
    var element: Element
    let step: Element.Stride
}

extension StrideIterator : IteratorProtocol where Element : Strideable {

    mutating func next() -> Element? {
        defer { element = element.advanced(by: step) }
        return element
    }
}

struct Stride<Element : Strideable> {
    var start: Element
    var step: Element.Stride
}

extension Stride : Sequence {

    typealias Iterator = StrideIterator<Element>

    func makeIterator() -> Iterator {
        return StrideIterator(element: start, step: step)
    }
}

extension Stride {

    func stop(before end: Element) -> StrideTo<Element> {
        return stride(from: start, to: end, by: step)
    }

    func stop(on end: Element) -> StrideThrough<Element> {
        return stride(from: start, through: end, by: step)
    }
}

func stride<Element: Strideable>(from start: Element, by step: Element.Stride) -> Stride<Element> {
    return Stride(start: start, step: step)
}


precedencegroup StrideStopPrecedenceGroup {
}

precedencegroup StridePrecedenceGroup {
    higherThan: StrideStopPrecedenceGroup
}

infix operator ++ : StridePrecedenceGroup

infix operator ..! : StrideStopPrecedenceGroup
infix operator ... : StrideStopPrecedenceGroup

func ++ <T: Strideable>(start: T, step: T.Stride) -> Stride<T> {
    return stride(from: start, by: step)
}

func ..! <T: Strideable>(stride: Stride<T>, stop: T) -> StrideTo<T> {
    return stride.stop(before: stop)
}

func ... <T: Strideable>(stride: Stride<T>, stop: T) -> StrideThrough<T> {
    return stride.stop(on: stop)
}

for x in -10 ++ 4 {
    print(x)
    break
}

for x in -10 ++ 4 ..! 10 {
    print(x)
}

print(Array((10 ++ -4).stop(before: -10)))
print(Array(-10 ++ 4 ..! 10))

Output:

-10
-10
-6
-2
2
6
[10, 6, 2, -2, -6]
[-10, -6, -2, 2, 6]

Nice idea! But, the ++ operator would mislead us because we well know the deprecated ++ operator. ..+ or .++ might be preferred because it is similar to ..., ..<, and ..!.

for x in -10 ..+ 4 {
    print(x)
    break
}
Array(10 ..+ -4 ... -10)
Array(-10 ..+ 4 ..! 10)

Also whatever syntax is decided upon should be usable with CountablePartialRangeFrom as well for variable stepping with no upper bound. It's actually somewhat surprising that there is no such thing as stride(from:_, by:_).

1 Like

I noticed no subscript of Array for stride.

var x = Array(0..<10)
x[stride(from: 1, through: 8, by: 2)] = [-1, -2, -3]
  // should be [0, -1, 2, -2, 4, -3, 6, 7, 8, 9]

gives error messages:

error: repl.swift:2:2: error: cannot subscript a value of type '[Int]' with an index of type 'StrideThrough<Int>'
x[stride(from: 1, through: 8, by: 2)] = [-1, -2, -3]
 ^

repl.swift:2:2: note: overloads for 'subscript' exist with these partially matching parameter lists: (Int), (Range<Int>), (Range<Self.Index>), ((UnboundedRange_) -> ())
x[stride(from: 1, through: 8, by: 2)] = [-1, -2, -3]
 ^

First, I made minor modification to Alexandre's implementation as the following:

precedencegroup StrideStopPrecedence {
    higherThan: RangeFormationPrecedence
}

precedencegroup StrideFormationPrecedence {
    higherThan: StrideStopPrecedence
    lowerThan: AdditionPrecedence
}

infix operator ..+ : StrideFormationPrecedence

infix operator ..! : StrideStopPrecedence
infix operator ... : StrideStopPrecedence

func ..+ <T: Strideable>(start: T, step: T.Stride) -> Stride<T> {
    return stride(from: start, by: step)
}

Then, I added an extension of Array to subscript Array with stride:

extension Array  {
    subscript (stride: Stride<Int>) -> Array<Element> {
        get {
            let stride = stride.stop(before: self.count)
            return stride.map { self[$0] }
        }
        set (newValues) {
            let stride = stride.stop(before: self.count)
            zip(stride, newValues).forEach { self[$0] = $1 }
        }
    }
    subscript (stride: StrideTo<Int>) -> Array<Element> {
        get {
            return stride.map { self[$0] }
        }
        set (newValues) {
            zip(stride, newValues).forEach { self[$0] = $1 }
        }
    }
    subscript (stride: StrideThrough<Int>) -> Array<Element> {
        get {
            return stride.map { self[$0] }
        }
        set (newValues) {
            zip(stride, newValues).forEach { self[$0] = $1 }
        }
    }
}

For example,

var x = Array(0..<10)
x[stride(from: 1, by: 2)]
x[1..+2]
// [1, 3, 5, 7, 9]

x[stride(from: 1, to: 8, by: 2)]
x[1..+2..!8]
// [1, 3, 5, 7]

x[stride(from: 1, through: 8, by: 2)]
x[1..+2...8]
// [1, 3, 5, 7]

x = Array(0..<10)
x[1..+2] = [-1, -2, -3]
// [0, -1, 2, -2, 4, -3, 6, 7, 8, 9]
x[1..+2] = [-1, -2, -3, -4]
// [0, -1, 2, -2, 4, -3, 6, -4, 8, 9]
x[1..+2] = [-1, -2, -3, -4, -5]
// [0, -1, 2, -2, 4, -3, 6, -4, 8, -5]

x = Array(0..<10)
x[1..+2..!8] = [-1, -2, -3]
// [0, -1, 2, -2, 4, -3, 6, 7, 8, 9]
x[1..+2..!8] = [-1, -2, -3, -4]
// [0, -1, 2, -2, 4, -3, 6, -4, 8, 9]
x[1..+2..!8] = [-1, -2, -3, -4, -5]
// [0, -1, 2, -2, 4, -3, 6, -4, 8, 9]

x = Array(0..<10)
x[1..+2...8] = [-1, -2, -3]
// [0, -1, 2, -2, 4, -3, 6, 7, 8, 9]
x[1..+2...8] = [-1, -2, -3, -4]
// [0, -1, 2, -2, 4, -3, 6, -4, 8, 9]
x[1..+2...8] = [-1, -2, -3, -4, -5]
// [0, -1, 2, -2, 4, -3, 6, -4, 8, 9]

Is there another implementation for the subscript?

Maybe, we need a new generic structure like an ArraySlice with a stride for
subscript (stride:) -> ArrayStrideSlice { get }.

Maybe, we need a Slice structure with a stride, which might be named as StrideSlice.

Here is the definition of the Slice structure from swift/stdlib/public/core/Slice.swift.

public struct Slice<Base: Collection> {
  public var _startIndex: Base.Index
  public var _endIndex: Base.Index
  internal var _base: Base
  public init(base: Base, bounds: Range<Base.Index>) {
    self._base = base
    self._startIndex = bounds.lowerBound
    self._endIndex = bounds.upperBound
  }
  public var base: Base {
    return _base
  }
}

The Index of Collection is Comparable .

public protocol Collection: Sequence where SubSequence: Collection {
  associatedtype Index : Comparable
}

So, StrideSlice might be defined as:

public struct StrideSlice <Base: Collection> {
  public var _startIndex: Base.Index
  public var _endIndex: Base.Index
  public var _stride: Base.Index.Stride
  internal var _base: Base
  public init(base: Base, strideTo: StrideTo<Base.Index>) {
    self._base = base
    self._startIndex = strideTo.start
    self._endIndex = strideTo.end
    self._stride = strideTo.stride
  }
  public var base: Base {
    return _base
  }
}

The Index of Collection should be Strideable.

public protocol Collection: Sequence where SubSequence: Collection {
    associatedtype Index : Strideable
}

FWIW StrideTo and StrideThrough are both Sequence's, so the subscript functionality could be done by generalising this functionality onto a Collection taking a Sequence.

extension Collection {
  // should have some specialised return type instead of Array<Element>
  subscript<S: Sequence>(seq: S) -> [Element]
  where S.Element == Index {
    var result = [Element]()
    for i in seq {
      result.append(self[i])
    }
    return Array(result)
  }
}

let x = Array(1...10)

print(x[stride(from: 0, to: 10, by: 2)])
print(x[sequence(first: 0) { $0 + 2 >= 10 ? nil : $0 + 2 } ])

This has the obvious added benefit of working with any sequence as a subscript, though I'm not sure why Swift doesn't have this already though. I can't see many downsides to having this, but I'm sure someone will point them out :slight_smile:

1 Like