alexis

software engineer, nyc

Byte of the Week: Lazy Collections

It can be tricky to optimize operations when working with sequences. One way to achieve better performance with collections in Swift is to use their lazy counterpart to perform operations such as mapping and filtering.

Lazy collections are collections where the results of some operations are combined into a single value, thus avoiding creating multiple intermediary collections. For example, if you call map twice on an array, a lazy collection will combine the results of these calls into one array, instead of returning an array as the result of the first map, using that array in the second map call, and then returning a second array.

You create a lazy collection by calling lazy on the base collection. To understand if a lazy collection is beneficial to your use case, you should remember the distinction between these two categories of operations:

  • Lazy operations: operations that can be performed in place and that do not affect the order of the elements:
    • map
    • flatMap
    • compactMap
    • filter
    • joined
    • prefix(while:)
    • drop(while:)
  • Eager operations: all the other operations that must return an intermediary collection to ensure data consistency, or that convert a lazy collection to a normal collection.

The code behind lazy operations is not called until just before an eager operation is performed.

Caveats

  • You cannot use Int indices directly by subscripting on a filter lazy collection because it defers subscripting to the base collection. Use index(_:offsetBy:) instead, like you would for a String. [1]

Example

You typically should use a lazy collection if you perform one or more lazy operations followed by an eager operation. A good example is if you need to use both a filter and a transformation:

let users = [
    "High School Musical": [
        User(name: "Troy", age: 17),
        User(name: "Gabriella", age: 16),
        User(name: "Sharpay", age: 18),
        User(name: "Ryan", age: 18)
    ],
    "Wizards of Waverly Place": [
        User(name: "Justin", age: 19),
        User(name: "Alex", age: 17),
        User(name: "Harper", age: 17),
        User(name: "Max", age: 14)
    ]
]

let adults = users
    .values
    .lazy
    .joined()
    .filter {
        $0.age >= 18
    }
    .sorted {
        $0.name < $1.name
    } // joined and filter are not called until sorted is called.

Sources

  1. Harshil Shah, Twitter