How and when to use Lazy Collections in Swift

Lazy collections are similar to a regular collection but change the way how modifiers like map, filter, and reduce are processed. In my experience, they haven’t got as much attention as they should as they can be more performant in certain cases.

You might be more familiar with lazy vars, but have you used the lazy property on sequences before? I’ll explain to you what lazy collections are and when you should use them.

What is a lazy collection?

A lazy collection postpones calculations until they are actually needed. This can be beneficial in many different cases and prevent doing unneeded work if elements are never being asked in the end.

The following example shows a collection of numbers in which even numbers are doubled. Without using the lazy keyword, all items would be processed directly upon creation:

 var numbers: [Int] = [1, 2, 3, 6, 9]
 let modifiedNumbers = numbers
     .filter { number in
         print("Even number filter")
         return number % 2 == 0
     }.map { number -> Int in
         print("Doubling the number")
         return number * 2
     }
 print(modifiedNumbers)
 /*
  Even number filter
  Even number filter
  Even number filter
  Even number filter
  Even number filter
  Doubling the number
  Doubling the number
  [4, 12]
  */

As you can see, the doubling of the two even numbers happens after all 5 numbers are filtered.

If we would add the lazy keyword to make the array compute modifiers lazily, the outcome would be different:

 let modifiedLazyNumbers = numbers.lazy
     .filter { number in
         print("Lazy Even number filter")
         return number % 2 == 0
     }.map { number -> Int in
         print("Lazy Doubling the number")
         return number * 2
     }
 print(modifiedLazyNumbers)
 // Prints:
 // LazyMapSequence>, Int>(_base: Swift.LazyFilterSequence>(_base: [1, 2, 3, 6, 9], _predicate: (Function)), _transform: (Function))

In fact, the modifiers aren’t getting called at all! This is because we didn’t request any of the numbers yet. Modifiers like filter and map will only be executed upon requesting an element:

 print(modifiedLazyNumbers.first!)
 /*
  Prints:
  Lazy Even number filter
  Lazy Even number filter
  Lazy Doubling the number
  4
  */

You can imagine this can save you from a lot of work if only a few items are used from a big collection.

Handling output values on the go

Another benefit of lazy collections is the option to handle output values on the go. For example, imagine having an avatar image fetcher that you want to use to fetch avatars for usernames starting with the letter A.

Without lazy it would execute as follows:

 let usernames = ["Antoine", "Maaike", "Jaap", "Amber", "Lady", "Angie"]
 usernames
     .filter { username in
         print("filtered name")
         return username.lowercased().first == "a"
     }.forEach { username in
         print("Fetch avatar for (username)")
     }
 /*
  Prints:
  filtered name
  filtered name
  filtered name
  filtered name
  filtered name
  filtered name
  Fetch avatar for Antoine
  Fetch avatar for Amber
  Fetch avatar for Angie
  */

All names are filtered first, after which we fetch an avatar for all names starting with an A.

Although this works, we would only start fetching after the whole collection is filtered. This can be a downside if we have to iterate over a big collection of names.

Instead, if we would use a lazy collection in this scenario, we would be able to start fetching avatars on the go:

 let usernames = ["Antoine", "Maaike", "Jaap", "Amber", "Lady", "Angie"]
 usernames.lazy
     .filter { username in
         print("filtered name")
         return username.lowercased().first == "a"
     }.forEach { username in
         print("Fetch avatar for (username)")
     }
 /*
  Prints:
  filtered name
  Fetch avatar for Antoine
  filtered name
  filtered name
  filtered name
  Fetch avatar for Amber
  filtered name
  filtered name
  Fetch avatar for Angie
  */

It’s important to understand the differences between a lazy array and a regular array. Once you know when modifiers are executed, you can decide whether or not a lazy collection makes sense for your specific case.

Use opt-in over opt-out

Now that you’ve seen that lazy collections can be more performant, you might be thinking: “I’ll just use lazy everywhere!”. However, it’s important to understand the implications of using a lazy array.

Don’t over optimize

A collection having only 5 items won’t give you a lot of performance wins when using lazy. It’s a case-per-case decision, and it also depends on the amount of work done by your modifiers. In most cases, lazy will only be useful when you’re only going to use a few items of a large collection.

On top of that, it’s important to know that lazy arrays aren’t cached.

Lazy Collections don’t cache

A lazy collection postpones executing modifiers until they’re requested. This also means that the outcome values aren’t stored in an output array. In fact, all modifiers are executed again on each item request:

 let modifiedLazyNumbers = numbers.lazy
     .filter { number in
         print("Lazy Even number filter")
         return number % 2 == 0
     }.map { number -> Int in
         print("Lazy Doubling the number")
         return number * 2
     }
 print(modifiedLazyNumbers.first!)
 print(modifiedLazyNumbers.first!)
 /*
  Prints:
  Lazy Even number filter
  Lazy Even number filter
  Lazy Doubling the number
  4
  Lazy Even number filter
  Lazy Even number filter
  Lazy Doubling the number
  4
  */

While the same scenario with a non-lazy collection would compute output values only once:

 let modifiedNumbers = numbers
     .filter { number in
         print("Lazy Even number filter")
         return number % 2 == 0
     }.map { number -> Int in
         print("Lazy Doubling the number")
         return number * 2
     }
 print(modifiedNumbers.first!)
 print(modifiedNumbers.first!)
 /*
  Prints:
  Lazy Even number filter
  Lazy Even number filter
  Lazy Even number filter
  Lazy Even number filter
  Lazy Even number filter
  Lazy Doubling the number
  Lazy Doubling the number
  4
  4
  */

Take the delay into account

A lazy collection only performs its modifiers upon request. In case one of the modifiers performs tasks that can take time, you might want to step away from using lazy.

In other words, it might be beneficial to calculate output values upfront and have them ready when they’re actually needed. You don’t want to perform the heavy lifting while the user is scrolling, for example.

Consider using standard Swift APIs over lazy arrays

A topic on its own and another reason to reconsider using lazy collections. Swift provides us a whole API of optimized modifiers to work with collections that might be a better solution to your problem.

For example, you might think it’s a smart decision to use lazy in this scenario as it prevents us from filter all numbers before we start using only the first element:

 let collectionOfNumbers = (1…1000000)
 let lazyFirst = collectionOfNumbers.lazy
     .filter {
         print("filter")
         return $0 % 2 == 0
     }.first
 print(lazyFirst) // Prints: 2

However, in this case, we benefit from using first(where:) instead. It’s a standard Swift API and it allows us to benefit from all underlying (future) optimizations:

 let firstWhere = collectionOfNumbers.first(where: { $0 % 2 == 0 })
 print(firstWhere) // Prints: 2

I wrote a whole blog post about these decisions which you can read: Performance, functional programming and collections in Swift.

Conclusion

Lazy collections are a powerful element of Swift and can result in better performance for specific cases. It’s important to know its implications to decide whether or not lazy arrays are the right solution for your scenario.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!