KISS

Keep It Simple Stupid

An infinite list usage example in swift

| comments

Learning various programming paradigms is very useful to extend your mind and also design approaches in your main language. For example, this post describes a simple example where infinite lists, as in Haskell, allow us to solve a problem in swift more elegantly.

The simple task

Let’s say we have some basic data structure in our domain model:

1
2
3
4
5
6
7
8
9
10
11
public typealias Label = String

public struct ColoredLabel {
    public let label: Label
    public let color: UIColor

    public init(label: Label, color: UIColor) {
        self.label = label
        self.color = color
    }
}

We have a list of Labels, which can be of any length, and we need to add a color to every Label, create a new ColoredLabel and send it to another system. We have a predefined set of colors, let’s say only three of them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public let labels: [Label] = ["a", "b", "c", "d", "e", "f", "g"]

public let defaultColors: [UIColor] = [.blue, .green, .orange]

public extension UIColor {
    open override var debugDescription: String {
        switch self {
        case UIColor.blue: return "blue"
        case UIColor.green: return "green"
        case UIColor.orange: return "orange"
        case UIColor.black: return "black"
        default: return super.description
        }
    }
}

I’ve also overridden the debugDescription for our set of colors to make the debug output below more readable.

How do you color each label in case when we have more labels than colors, given that the colors should be looped?

Incorrect zipping

The first solution could be simple, like this:

1
2
3
4
5
6
7
8
9
10
let coloredLabels = zip(labels, Constants.defaultColors).map { (label, color) in
    ColoredLabel(label: label, color: color)
}

print("Colored labels: \(coloredLabels)")

// output:
// Colored labels: [ColoredLabel(label: "a", color: blue),
// ColoredLabel(label: "b", color: green),
// ColoredLabel(label: "c", color: orange)]

Unfortunately, it outputs only three colored labels instead of seven because zip stops when either of the sequences runs out of elements.

zipping with manually extended colors

We can manually extend the number of available colors to have the correct number of them to zip all our labels. If we ignore the constraint in our task for now (“the colors should be looped”), we could append the same color as many times as we need. How many times? For simplicity, we could append as many as there are labels:

1
2
3
4
5
6
7
8
9
10
11
12
13
let colors = defaultColors + Array(repeating: UIColor.black, count: labels.count)

let coloredLabels = zip(labels, colors).map { (label, color) in
    ColoredLabel(label: label, color: color)
}

// Colored labels: [ColoredLabel(label: "a", color: blue),
// ColoredLabel(label: "b", color: green),
// ColoredLabel(label: "c", color: orange),
// ColoredLabel(label: "d", color: black),
// ColoredLabel(label: "e", color: black),
// ColoredLabel(label: "f", color: black),
// ColoredLabel(label: "g", color: black)]

Yes, it works, but we’re appending more elements than necessary. We can calculate the right number of course:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let extraColorsCount = labels.count - defaultColors.count
let colors = defaultColors + Array(repeating: UIColor.black, count: extraColorsCount)

let coloredLabels = zip(labels, colors).map { (label, color) in
    ColoredLabel(label: label, color: color)
}

// Colored labels: [ColoredLabel(label: "a", color: blue),
// ColoredLabel(label: "b", color: green),
// ColoredLabel(label: "c", color: orange),
// ColoredLabel(label: "d", color: black),
// ColoredLabel(label: "e", color: black),
// ColoredLabel(label: "f", color: black),
// ColoredLabel(label: "g", color: black)]

Good, isn’t it? Not so good if we have more colors than labels:

1
2
3
let labels: [Label] = ["a"]

// `EXC_BAD_INSTRUCTION` fatal error: Can't construct Array with count < 0

Oh, right. Let’s make sure we don’t create arrays of negative length:

1
2
3
4
5
6
7
8
9
10
11
12
13
let labels: [Label] = ["a"]

let extraColorsCount = labels.count - defaultColors.count
let extraColors = (extraColorsCount > 0)
    ? Array(repeating: UIColor.black, count: extraColorsCount)
    : []
let colors = defaultColors + extraColors

let coloredLabels = zip(labels, colors).map { (label, color) in
    ColoredLabel(label: label, color: color)
}

// Colored labels: [ColoredLabel(label: "a", color: blue)]

That works in all cases now, but with a bunch of boilerplate code. Trying to incorporate the colors looping here would create even more code and more complex logic.

for loop

Probably a more swift-like (imperative) approach would be with a for loop. Here’s how it could be implemented:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var coloredLabels: [ColoredLabel] = []
var colorIndex = 0
for label in labels {
    let color = defaultColors[colorIndex % defaultColors.count]
    let coloredLabel = ColoredLabel(label: label, color: color)
    coloredLabels.append(coloredLabel)
    colorIndex += 1
}

// Colored labels: [ColoredLabel(label: "a", color: blue),
// ColoredLabel(label: "b", color: green),
// ColoredLabel(label: "c", color: orange),
// ColoredLabel(label: "d", color: blue),
// ColoredLabel(label: "e", color: green),
// ColoredLabel(label: "f", color: orange),
// ColoredLabel(label: "g", color: blue)]

Sure, this works and the color looping is easy to implement. However, I don’t like this solution for multiple reasons:

  1. It uses mutable variables, so the reader has to keep track of their state and know when and how they change.
  2. It has a lot (relative to the size of our task) of boilerplate syntax: declaring the variables and manually updating them.
  3. The code is unsafe and it is very easy to get a runtime exception if you use the wrong index somewhere, e.g. let color = defaultColors[colorIndex % (defaultColors.count + 1)] — relatively easy to spot in this tiny example, but harder in some real code.
  4. And finally, it violates the Single Responsibility Principle, we’re mixing up multiple concerns here: WHAT to do — creating a ColoredLabel and HOW to do it — managing the color index, calculating the current color, appending the new colored label to an array.

cycle and zip

Again, what we want is a list of looped colors, as many of them as necessary. However instead of creating the list beforehand (“push” style), we could use a list, which we could ask to get the next element, and the next, and the next, and so on (“pull” style). So we really need just an infinite list. In Haskell there is a function called cycle that cycles a finite list infinitely, it would be great to have it in swift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public extension Array {
    /// Cycles the elements in `self` infinitely.
    /// - warning: Do not call on an empty array.
    public func cycle() -> UnfoldSequence<Element, Int> {
        let count = self.count
        return sequence(state: 0, next: { (index: inout Int) in
            let x = self[index]
            index = (index + 1) % count
            return x
        })
    }
}

class ArrayCycleTests: XCTestCase {
    func testShouldProduceAsManyLoopedElementsAsNecessary() {
        let source = [0, 1, 2]
        let actual = source.cycle()
        let expected = [0, 1, 2, 0, 1, 2, 0]
        XCTAssertEqual(Array(actual.prefix(expected.count)), expected)
    }

    func testShouldWorkWithSingletonArray() {
        let anonymousNumber = 42
        let actual = [anonymousNumber].cycle()
        let expected = Array(repeating: anonymousNumber, count: 4)
        XCTAssertEqual(Array(actual.prefix(expected.count)), expected)
    }
}

Now the cycle implementation is abstracted, we don’t care how it works as long as it does what it should do. The solution to our task is very simple now:

1
2
3
let coloredLabels = zip(labels, defaultColors.cycle()).map { (label, color) in
    ColoredLabel(label: label, color: color)
}

We’ve separated the concerns here: what to do — creating a ColoredLabel in the lambda, and the data to use — cycle and zip.

Don’t forget that you can’t iterate over the whole infinite list, you must have a terminating condition, for example by using prefix().

cycle on an empty array?

One note: this cycle implementation will crash if called on an empty array. Well, calling it that way doesn’t make any sense, what would it do?

  1. It could return an empty sequence, but that would violate the expected behavior.
  2. We can add a precondition with a meaningful message, but that’s still a runtime error.
  3. The best solution is to follow the types and implement it only for non-empty arrays. Unfortunately, that type is not in the swift’s standard library, and you can find an implementation online — this guarantees type safety at compile time, no more runtime errors!

Conclusion

I believe this solution using cycle and zip is very neat and elegant, and is actually easier to understand than the others in this post. Yes, there is a small barrier to understanding infinite lists if you’ve never seen them, but once you do, they become obvious to you.

Even in this trivial task the solution with infinite lists is simpler than others. If you recognize similar problems and can apply the same approach to bigger tasks, you’ll have even bigger benefit in the clarity of your code.

Comments