Why and when to use lazy with Array in Swift?
Asked Answered
S

3

60
[1, 2, 3, -1, -2].filter({ $0 > 0 }).count // => 3

[1, 2, 3, -1, -2].lazy.filter({ $0 > 0 }).count // => 3

What is the advantage of adding lazy to the second statement. As per my understanding, when lazy variable is used, memory is initialized to that variable at the time when it used. How does it make sense in this context?

enter image description here

Trying to understand the the use of LazySequence in little more detail. I had used the map, reduce and filter functions on sequences, but never on lazy sequence. Need to understand why to use this?

Serrate answered 19/8, 2018 at 11:13 Comment(5)
lazy initialization is usually use for heavy elements like UIKit elements. Haven't seen it being used with arrays.Marigolda
I've never seen that use of lazy before. Where did you learn about it?Jobie
I was readying SE - 0220 proposal, where I come across this. github.com/apple/swift-evolution/blob/master/proposals/…Serrate
Maybe if the arrays are big and the filtering takes a lot of time, you don't want to be filtering the array immediately?Marigolda
For a comprehensible description please watch WWDC 2018: Using Collections Effectively from 15:00Boabdil
A
94

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.

Ankylosaur answered 19/8, 2018 at 11:59 Comment(9)
This is a nice thorough answer. One basic example of a time I sometimes think of using it is when I want the '.first' element of the array. It can potentially save a lot of work from being done.Valediction
Really detailed discussion and steps to prove for the usage of lazy and mechanism behind it. Upvoting!Photokinesis
as of Swift 5.2 running only on iOS 13.4+ devices, this behavior has slightly changed, the order of Sequence functions are now reversed, meaning that upstream function will finish first before executing the downstream function, a simple example can be found here. so I believe that the third point in your answer is no longer valid.Wistful
To be more specific, the 5.2 changes are here: github.com/apple/swift/blob/master/…Dorison
@JAHelia, that is an interesting change and a correct bug fix. I wasn't aware of this multiple filter bug so it doesn't really change my third point. I was just trying to note that the final user of the filtered values can get started with side effects sooner because values will be passed along before all of the upstream filters have finished. For instance, if you were running these filters on a very large array in a background thread, and then were printing the results in the foreground, you could start updating the UI immediately instead of having to wait for the filters to finish.Ankylosaur
Thanks @MichaelOzeryansky. This is useful information.Ankylosaur
Doesn't the change only apply to multiple chained filters? Reducing after filtering should be unchanged imo.Cornall
@FrederikWinkelsdorf, even just one filter followed by reduce demonstrates the difference as my example above shows. The key is whether filter creates an array of the filtered values before the reduce runs, or with lazy it passes each filtered value on to reduce immediately as the are generated.Ankylosaur
@Ankylosaur Thanks for your reply! Absolutely right, the intermediate array is omitted, probably a misunderstanding: I meant the introduced change in the Sequence order with Swift 5.2 (or on Devices < iOS 13.4). I think that's nothing to worry about, if multiple filters aren't chained. So .lazy.filter.reduce should be safe to use on any known environment.Cornall
J
9

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.

Jobie answered 19/8, 2018 at 11:58 Comment(2)
Not always slower if you don't need to process the entire collection. (1...1000000).lazy.filter{ $0 % 2 == 0}.first is much faster with lazy than without.Ankylosaur
Actually, if you have multiple steps on the filtered array, lazy is slower. See the edit to my answer. I see what you're saying about only dealing with a small portion of the filtered result though.Jobie
P
0

💡 At first sight, It seems lazy is a perfect tool to optimize all collection operations but there are cases where it is actually the opposite. So besides other great answers, I want to add:

Why and when " NOT ! " to use lazy with Array in Swift

Here are some of them:

1. When you need a result along the way

Take a look at this example This will iterate exactly 20 times:

(0...9)
  /* .lazy */
  .map { $0 * 2 }
  .filter { $0 > 10 }
  .prefix(3)
  .count

It seems it should do it only 3 times by uncommenting lazy, but it will be increased to 40 times !

💡 you can combine modifiers instead for fewer iterations:

(0...9)
  .compactMap {
      let r = $0 * 2
      return r > 10 ? r : nil
  }
  .prefix(3)

2. When NOT using a loop breaker:

It will almost always be a waste of time if you don't use a loop breaker like first, last, allSatisfy etc. along the way of operation chaining. Also, it causes each operator to be performed more than once!


3. When there is a dedicated standard API

Instead of chaining operators on a lazy collection, almost always it's better to use the dedicated API.

So for example instead of:

.lazy.filter { someCondition }.first

Just use this:

.first(where: { someCondition })

This will help Swift to future optimize the code.


4. When the entire result is needed upfront

Although using lazy will delay the operations and is very beneficial in many cases, you should consider postponing heavy operations may cause undesired delays! For example, you can do the entire calculation upfront instead of blocking the user periodically. (Think of scrolling a list for example)

These are just samples of where you should NOT use the lazy collection. But the list does not end here and the usage is very specific to the use case.

Hope it helps

Prodigious answered 24/7, 2023 at 21:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.