2

In Swift, we can use range operators (... and ..<) to loop over ranges:

for i in 0...4 { ... }

Assuming I have many colors:

let red: MyColor
let orange: MyColor
let yellow: MyColor
let green: MyColor
let blue: MyColor
let indigo: MyColor
let violet: MyColor

let black: MyColor
let brown: MyColor

How can I support the range operator so that I can loop over the colors in rainbow order?

for rainbowColor in red...violet { ... }
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • I believe this to be do-able. I'm working on some code in a playground. – nhgrif Aug 11 '16 at 23:58
  • 1
    Also, please don't repost questions just because one person marked the question as a duplicate. – nhgrif Aug 12 '16 at 00:06
  • @nhgrif Yeah sorry about that, but it was 3+ people that weren't reading the question thoroughly, and it already had the banner up top saying it was a duplicate. I figured not too many people saw it yet so I deleted the old and created a new one. – Senseful Aug 12 '16 at 00:08
  • Related: [Add “for in” support to iterate over Swift custom classes](http://stackoverflow.com/questions/24099227/add-for-in-support-to-iterate-over-swift-custom-classes) – Senseful Aug 12 '16 at 22:11

3 Answers3

2

(Note that this is Swift 3. If you want Swift 2.2, see Senseful's answer, which is basically the same with the old protocols.)

You want Color to be Strideable with a Stride of Int. Then you get ... and ..< for free. Let's do that.

First, I'm going to assume your type is something like this. This is just an example. You could do all kinds of things, as long as you can implement Strideable. You seem to want structs rather than enums (which is fine), so let's do structs. That just means other colors could be added by anyone and you have to consider that in your design. Colors need something to distinguish them, so we'll give them a name. If they were classes, then that would also allow them to be distinguished.

struct Color {
    let name: String

    static let red = Color(name: "red")
    static let orange = Color(name: "orange")
    static let yellow = Color(name: "yellow")
    static let green = Color(name: "green")
    static let blue = Color(name: "blue")
    static let indigo = Color(name: "indigo")
    static let violet = Color(name: "violet")
}

First rule of Strideable is Equatable.

extension Color: Equatable {
    static func == (lhs: Color, rhs: Color) -> Bool {
        return lhs.name == rhs.name
    }
}

Now that it's possible equate, we should come up with what we mean by "order." Here's one way. Since structs make unexpected values possible, I've said that those are all equal and order before any of my rainbow colors. If this were an enum, we wouldn't have to worry about that case.

extension Color {
    private static let _order: [Color] = [red, orange, yellow, green, blue, indigo, violet]

    private var _indexValue: Int {
        return Color._order.index(of: self) ?? Int.min
    }
}

So with that, we can answer question #2: Comparable:

extension Color: Comparable {
    static func < (lhs: Color, rhs: Color) -> Bool {
        return lhs._indexValue < rhs._indexValue
    }
}

And just one more little step to Strideable:

extension Color: Strideable {
    func advanced(by n: Int) -> Color {
        return Color._order[self._indexValue + n]
    }

    func distance(to other: Color) -> Int {
        return other._indexValue - _indexValue
    }
}

And ta da, your ...:

for rainbowColor: Color in .red ... .violet { print(rainbowColor) }

Now I happen to like enums a lot, and they happen to have a bit of a trick if you try to implement this with an enum, so it's worth noting. For example, say you had:

enum Color {
    case red
    case orange
    case yellow
    case green
    case blue
    case indigo
    case violet
}

It turns out that you can't use index(of:) for computing _indexValue. You get yourself into an infinite loop because it calls distance(to:) in the default == implementation (I'm not actually certain why this happens yet). So you have to implement _indexValue this way:

private var _indexValue: Int {
    switch self {
    case .red: return 0
    case .orange: return 1
    case .yellow: return 2
    case .green: return 3
    case .blue: return 4
    case .indigo: return 5
    case .violet: return 6
    }
}

Huh; that looks an awful lot like a raw value. Yeah. This whole approach is basically attaching raw values from the side without having to modify the type itself. So that's why it's going to seem really similar. The rest is the same as for a struct, and the approach can be applied to anything else that you can figure out how to stride.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • This is great, thanks! Btw, do you know of a way to get it working with just the `Comparable` protocol as I mentioned in my answer? – Senseful Aug 12 '16 at 00:47
2

(Note: This is for Swift 2.2. If you want a Swift 3 version, see Rob's answer.)

Swift already defines the following functions:

/// Forms a closed range that contains both `start` and `end`.
/// - Requires: `start <= end`.
@warn_unused_result
public func ...<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos) -> Range<Pos>

/// Forms a closed range that contains both `minimum` and `maximum`.
@warn_unused_result
public func ...<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos) -> Range<Pos>

/// Returns a closed interval from `start` through `end`.
@warn_unused_result
public func ...<Bound : Comparable>(start: Bound, end: Bound) -> ClosedInterval<Bound>

That means all we really need to do is conform to the ForwardIndexType. As Rob mentioned, it won't work with the Comparable protocol since that returns an Interval and we need a Range for a for/in loop.

Assume this is our shared code:

import UIKit

struct MyColor {
  let title: String

  init(title: String) {
    self.title = title
  }
}

let red = MyColor(title: "Red")
let orange = MyColor(title: "Orange")
let yellow = MyColor(title: "Yellow")
let green = MyColor(title: "Green")
let blue = MyColor(title: "Blue")
let indigo = MyColor(title: "Indigo")
let violet = MyColor(title: "Violet")

let white = MyColor(title: "White")
let lightGray = MyColor(title: "Light Gray")
let darkGray = MyColor(title: "Dark Gray")
let black = MyColor(title: "Black")

// TODO: insert protocol conformance here!

print("Rainbow colors:")
for color in red...violet {
  print(color.title)
}

print("")
print("Grayscale colors:")
for color in white...black {
  print(color.title)
}

With the output being:

Rainbow colors:
Red
Orange
Yellow
Green
Blue
Indigo
Violet

Grayscale colors:
White
Light Gray
Dark Gray
Black

We can make ... work by conforming to the ForwardIndexType protocol:

let nullColor = MyColor(title: "Null")

extension MyColor: Equatable {}
@warn_unused_result
func ==(lhs: MyColor, rhs: MyColor) -> Bool {
  return lhs.title == rhs.title
}

extension MyColor: ForwardIndexType {
  @warn_unused_result
  func successor() -> MyColor {
    switch self {
    case red: return orange
    case orange: return yellow
    case yellow: return green
    case green: return blue
    case blue: return indigo
    case indigo: return violet
    case violet: return nullColor

    case white: return lightGray
    case lightGray: return darkGray
    case darkGray: return black
    case black: return nullColor

    case nullColor: fatalError()
    default: fatalError()
    }
  }
}

Note: the nullColor (or some terminator) is required even if you only loop from red...violet. It seems that the system will actually execute the violet case even though the range clearly ends at violet.

Also note that this will only work with struct, enums, or final classes. If you attempt to change the above struct to a class, you'll get the following error:

execution failed: protocol 'ForwardIndexType' requirement '_failEarlyRangeCheck(_:bounds:)' cannot be satisfied by a non-final class ('MyColor') because it uses 'Self' in a non-parameter, non-result type positionmethod 'advancedBy' in non-final class 'MyColor' must return `Self` to conform to protocol 'ForwardIndexType'method 'advancedBy(_:limit:)' in non-final class 'MyColor' must return `Self` to conform to protocol 'ForwardIndexType'
error: method 'successor()' in non-final class 'MyColor' must return `Self` to conform to protocol '_Incrementable'
  func successor() -> MyColor {
       ^

For more information as to why this won't work, see Rob's answer here.

You should theoretically be able to get it working by implementing the Comparable protocol as well, but when I tried, I got:

error: binary operator '...' cannot be applied to two 'MyColor' operands
for color in red...violet {
             ~~~^  ~~~~~~
note: overloads for '...' exist with these partially matching parameter lists: (Bound, Bound), (Pos, Pos)
for color in red...violet {
                ^

Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • Comparable works for `...` but returns an Interval. You need a Range for for-in, which is why you need ForwardIndexType. Intervals handle things like `1.2 ... 3.5`, which isn't something you can iterate over. – Rob Napier Aug 12 '16 at 00:48
  • Ah, makes sense @RobNapier. Thanks for the explanation! – Senseful Aug 12 '16 at 00:49
  • I'd recommend that `black.successor()` crash rather than enter an infinite loop. `Int.max + 1 != Int.max`. You could of course define `MyColor.max` to equal `black`. – Rob Napier Aug 12 '16 at 00:53
  • @RobNapier Good point. I updated it to crash instead of going into an infinite loop. However, I did still need to use a terminating value (i.e. `nullColor`). If I try to change it to `case black: fatalError()`, even a valid range (`white...black`) causes a crash. – Senseful Aug 12 '16 at 01:03
  • Right… this is a quirk of `...` in Swift 2.2 that they got rid of in Swift 3. The way `x...y` is implemented in Swift 2 is as `x..<(y.successor())`. It's not possible in Swift 2.2 to include the maximal value of a type in a range, but it is in Swift 3. – Rob Napier Aug 12 '16 at 01:07
  • @RobNapier: thanks for the explanation. And thanks for preemptively answering my next question: how to do this with `class` instead of `struct`. I linked to your answer which, I believe, explains why this doesn't work with a non-final class. If you think there is a way to do this in Swift 2.2 with the `ForwardIndexType` protocol with a non-final class, let me know, and I can ask the question. – Senseful Aug 12 '16 at 01:32
1

So... you can do a for-in loop over any type. All you have to do is implement something conforming to SequenceType. Then, once you've implemented that class or struct, it's as simple as creating a ... operator that returns that SequenceType.

Assuming the following:

struct RainbowColor {
    let name: String
}

let red: RainbowColor = RainbowColor(name: "red")
let orange: RainbowColor = RainbowColor(name: "orange")
let yellow: RainbowColor = RainbowColor(name: "yellow")
let green: RainbowColor = RainbowColor(name: "green")
let blue: RainbowColor = RainbowColor(name: "blue")
let indigo: RainbowColor = RainbowColor(name: "indigo")
let violet: RainbowColor = RainbowColor(name: "violet")

We can create a RainbowColorSequence as such:

struct RainbowColorSequence: SequenceType {

    let startColor: RainbowColor
    let endColor: RainbowColor

    init(start: RainbowColor, end: RainbowColor) {
        startColor = start
        endColor = end
    }

    func generate() -> AnyGenerator<RainbowColor> {
        return AnyGenerator<RainbowColor> {
            // TODO: Implement your generator logic.
            return nil
        }
    }

}

(Note, the actual logic of what order to return the colors in and when the sequence ends is omitted from this... because it'd be messy. Read to the bottom for an example of what this implementation might look like, however.)

Once we have this RainbowColorSequence, the ... function is pretty simple:

func ...(lhs: RainbowColor, rhs: RainbowColor) -> RainbowColorSequence {
    return RainbowColorSequence(start: lhs, end: rhs)
}

And now, you can write this code:

for color in red...violet {
    print(color.name)
}

For an example on what your generator logic might look like, check out this question. I could easily add a ... operator to the code behind that link as all the rest of the logic is already in place, and ... is really just a light wrapper around the initialization of the SequenceType.

Importantly, you're going to have to write the logic for the sequence yourself. Given your conditions, there's no easy way for the compiler to infer what's in the sequence or what order the sequence comes in. And that logic probably gets quite messy depending on exactly what sort of type you're wanting to iterate over.

Community
  • 1
  • 1
nhgrif
  • 61,578
  • 25
  • 134
  • 173
  • Great answer, thank you! I'm also wondering if there's a way to do this without having to define a new struct (i.e. `RainbowColorSequence`). Going to try some stuff in the playground as well. – Senseful Aug 12 '16 at 00:13
  • Maybe? I mean, you could have the `...` operator return an array of colors, and then have lots of logic in that function to determine what to put in the array you're returning? I think the struct is probably the cleanest and most reusable approach. – nhgrif Aug 12 '16 at 00:14