Why and when to use lazy with Array in Swift?

lazy changes the way the array is processed. When lazy is not used, filter processes the entire array and stores the results into a new array. When lazy is used, the values in the sequence or collection are produced on demand from the downstream functions. The values are not stored in an array; they are just produced when needed.

Consider this modified example in which I've used reduce instead of count so that we can print out what is happening:

Not using lazy:

In this case, all items will be filtered first before anything is counted.

[1, 2, 3, -1, -2].filter({ print("filtered one"); return $0 > 0 })
    .reduce(0) { (total, elem) -> Int in print("counted one"); return total + 1 }
filtered one
filtered one
filtered one
filtered one
filtered one
counted one
counted one
counted one

Using lazy:

In this case, reduce is asking for an item to count, and filter will work until it finds one, then reduce will ask for another and filter will work until it finds another.

[1, 2, 3, -1, -2].lazy.filter({ print("filtered one"); return $0 > 0 })
    .reduce(0) { (total, elem) -> Int in print("counted one"); return total + 1 }
filtered one
counted one
filtered one
counted one
filtered one
counted one
filtered one
filtered one

When to use lazy:

option-clicking on lazy gives this explanation:

pop-up for lazy in Xcode

From the Discussion for lazy:

Use the lazy property when chaining operations:

  1. to prevent intermediate operations from allocating storage

    or

  2. when you only need a part of the final collection to avoid unnecessary computation

    I would add a third:

  3. when you want the downstream processes to get started sooner and not have to wait for the upstream processes to do all of their work first

So, for example, you'd want to use lazy before filter if you were searching for the first positive Int, because the search would stop as soon as you found one and it would save filter from having to filter the whole array and it would save having to allocate space for the filtered array.

For the 3rd point, imagine you have a program that is displaying prime numbers in the range 1...10_000_000 using filter on that range. You would rather show the primes as you found them than having to wait to compute them all before showing anything.


I hadn't seen this before so I did some searching and found it.

The syntax you post creates a lazy collection. A lazy collection avoids creating a whole series of intermediate arrays for each step of your code. It isn't that relevant when you only have a filter statement it would have much more effect if you did something like filter.map.map.filter.map, since without the lazy collection a new array is created at each step.

See this article for more information:

https://medium.com/developermind/lightning-read-1-lazy-collections-in-swift-fa997564c1a3

EDIT:

I did some benchmarking, and a series of higher-order functions like maps and filters is actually a little slower on a lazy collection than on a "regular" collection.

It looks like lazy collections give you a smaller memory footprint at the cost of slightly slower performance.

Edit #2:

@discardableResult func timeTest() -> Double {
    let start = Date()
    let array = 1...1000000
    let random = array
        .map { (value) -> UInt32 in
            let random = arc4random_uniform(100)
            //print("Mapping", value, "to random val \(random)")
            return random
    }
    let result = random.lazy  //Remove the .lazy here to compare
        .filter {
            let result = $0 % 100 == 0
            //print("  Testing \($0) < 50", result)
            return result
        }
        .map { (val: UInt32) -> NSNumber in
            //print("    Mapping", val, "to NSNumber")
            return NSNumber(value: val)
        }
        .compactMap { (number) -> String? in
            //print("      Mapping", number, "to String")
            return formatter.string(from: number)
        }
        .sorted { (lhv, rhv) -> Bool in
            //print("        Sorting strings")
            return (lhv.compare(rhv, options: .numeric) == .orderedAscending)
    }
    
    let elapsed = Date().timeIntervalSince(start)
    
    print("Completed in", String(format: "%0.3f", elapsed), "seconds. count = \(result.count)")
    return elapsed
}

In the code above, if you change the line

let result = random.lazy  //Remove the .lazy here to compare

to

let result = random  //Removes the .lazy here

Then it runs faster. With lazy, my benchmark has it take about 1.5 times longer with the .lazy collection compared to a straight array.