Lazy sequence, unexpected result

Hello! Does anybody have an idea why largeSqs[50] is 2500 (5050) and not 1104601 (10511051)
let allNmbrs = 1..<10_000
let largeSqs = allNmbrs.lazy
.filter { (element) -> Bool in
print(element)
return element > 1000
}.map { (element) -> Int in
print(element)
return element * element
}

largeSqs[50] // 2500

Swift uses Index when accessing an element in a Collection like array and dictionary.

For Array specifically, the index is guaranteed to behave the same as in good-ol C. That is, it starts from zero, and increments by one at every element. So things like array[2] is predictible.

For other collections, though, there is no such guarantee. In those cases, you need to create a valid index via index APIs. Doing largeSqs[50] is ill-formed because you are not accessing it with a valid index. In your case, you start from the start index, and offset it by 50 elements:

let startIndex = largeSqs.startIndex
let index = largeSqs.index(startIndex, offsetBy: 50)
largeSqs[index] // 1104601

This is how you generally traverse a collection, you start from a valid index, like start index and end index, and then move/offset around.


One caveat of using lazy is that the computation kicks in every time you offset the index, which is quite wasteful. If you calculate the index multiple times, you may want to convert the entire lazy chain into a simple Array first.

In this particular case, we can even do one better. Note that once the filter condition (element > 1000) is met, it continues to be satisfied until the end of the collection. So we can simply find the first element that meets the criteria, and the get a slice of the collection:

// The first index that meets the condition.
// If no element meets the condition, then set it to `endIndex`
let firstIndex = allNmbrs.firstIndex { $0 > 1000 } ?? allNmbrs.endIndex

let largeSqs = allNmbrs[firstIndex...] // Every element after `firstIndex`, inclusive
    .lazy // Make it lazy
    .map { (element) -> Int in
        print("map", element)
        return element * element
    }

let startIndex = largeSqs.startIndex
let index = largeSqs.index(startIndex, offsetBy: 50)
largeSqs[index] // 1104601

PS

You can put code in triple tick mark

```
Like this
```

and it will be rendered

Like this

Thanks for response and advice! But I still cannot understand why in the original code the filter part was just ignored and I got 50 * 50, not 1051 * 1051. As if there were no filter part

let allNmbrs = 1..<10_000
let largeSqs = allNmbrs.lazy
.filter { (element) -> Bool in
print(element)
return element > 1000
}.map { (element) -> Int in
print(element)
return element * element
}

largeSqs[50] // 2500

Many collections that wrap other collections use the same index as the underlying storage for fast access. ArraySlice is a notable example. LazySequence is also one of them. So when you do largeSgs[50], it treats 50 as the index of the underlying collection (allNmbrs).

It should actually crash the program since the element at index 50 is not part of largeSqs, though I suspect that the library leaves the checking out for performance reason. As said earlier, don't do this since it's ill-formed.

1 Like

To put it explicitly, there is absolutely no guarantee as to what largeSqs[50] will return. In a future version of Swift, it may crash, it may give you 2500, or it may give you 9 or 10000 for that matter.

1 Like