Senseful Senseful - 3 months ago 9
Swift Question

Adding support for the range operator in swift? (e.g. for x in red...violet)

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 { ... }

Answer

(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.

Comments