Swift Lazy Subscript Ignores Filter

Swift lazy subscript ignores filter

It comes down to subscripting a LazyFilterCollection with an integer which in this case ignores the predicate and forwards the subscript operation to the base.

For example, if we're looking for the strictly positive integers in an array :

let array = [-10, 10, 20, 30]
let lazyFilter = array.lazy.filter { $0 > 0 }

print(lazyFilter[3]) // 30

Or, if we're looking for the lowercase characters in a string :

let str = "Hello"
let lazyFilter = str.lazy.filter { $0 > "Z" }

print(lazyFilter[str.startIndex]) //H

In both cases, the subscript is forwarded to the base collection.

The proper way of subscripting a LazyFilterCollection is using a LazyFilterCollection<Base>.Index as described in the documentation :

let start = lazyFilter.startIndex
let index = lazyFilter.index(start, offsetBy: 1)
print(lazyFilter[index])

Which yields 20 for the array example, or l for the string example.


In your case, trying to access the index 3:

let start = empty.startIndex
let index = empty.index(start, offsetBy: 3)
print(empty)

would raise the expected runtime error :

Fatal error: Index out of range

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.

How do I access a lazy sequence with an index?

You can access them in a similar fashion to how you access a string by an integer, i.e. using index(_:offsetBy:):

filtered[filtered.index(filtered.startIndex, offsetBy: 2)]

Accessing a lazy filter sequence is quite similar to accessing a string in that both are not O(1).

Alternatively, drop the first n items and then access the first item:

filtered.dropFirst(2).first

But I prefer the first one as "dropping" things just to access a collection, in my mind, is quite weird.

Count optional properties in Swift

You can do this manually, with a convenience method:

func numberOfNonNil() -> Int {
let vals = [val1, val2, val3, val4, val5]
return flatMap { $0 }.count
}

flatMap(_:) takes a closure that takes a single element of the array and returns an optional value (Element -> T?), and returns the result of applying that closure to each element of the array, with nil values ignored.



The only way to make this simpler would be to store your values as an array of optional Ints in the first place:

class Test {
var vals: [Int?]
}

You can then still access each individual value using the array subscript notation (let val2 = vals[1]). You could then just use the second line in the convenience method above (filter then count) to get the number of non-nil values:

let nonNilCount = vals.flatMap { $0 }.count

If your values are of different types, this approach will still work if you cast the array to a type that encompasses all the different types:

class Test {
var val1: Int?
var val2: Double
var val3: String
var val4: MyRandomClass?

func numberOfNonNil() -> Int {
let vals = [val1, val2, val3, val4, val5] as [Any?]
return flatMap { $0 }.count
}
}

This works because all the values can be expressed as the type Any?.

Reverse Zip a Collection

This is a seed of an idea. Instead of returning arrays, it would be more efficient to return sequences.

Here is an Array extension that returns two LazyMapSequence<StrideTo<Int>, Element>. The advantage is that is doesn't actually generate the sequences; they are provided on demand because they're lazy. This is much like the way reversed() works on an Array.

extension Array {

func reverseZip() -> (LazyMapSequence<StrideTo<Int>, Element>, LazyMapSequence<StrideTo<Int>, Element>) {

let left = stride(from: 0, to: self.count, by: 2).lazy.map { self[$0] }
let right = stride(from: 1, to: self.count, by: 2).lazy.map { self[$0] }

return (left, right)
}
}

Example:

let a = [1, 2, 3, 4, 5, 6, 7]
let (left, right) = a.reverseZip()

// iterate left
for i in left {
print(i)
}
1
3
5
7
// turn them into arrays to print them
print(Array(left))
[1, 3, 5, 7]
print(Array(right))
[2, 4, 6]

I'm sure this can be generalized to something more general than Array. That is left as an exercise to the reader.


If you want two arrays

If you really want the two arrays, then you'd just return the result of map (taking out lazy):

extension Array {

func reverseZip() -> ([Element], [Element]) {

let left = stride(from: 0, to: self.count, by: 2).map { self[$0] }
let right = stride(from: 1, to: self.count, by: 2).map { self[$0] }

return (left, right)
}
}

SDWebImage process images before caching

I had the same problem as you, and tried tweaking SDWebImage first, but ended up building my own component that solved the problem. You can take take a look at it here : https://github.com/adig/RemoteImageView

Split Big Array Into Two Arrays

There are various fancy ways to do it with filter but most would probably require two passes rather than one, so you may as well just use a for-loop.

Reserving space up-front could make a big difference in this case since if the source is large it’ll avoid unnecessary re-allocation as the new arrays grow, and the calculation of space needed is in constant time on arrays.

// could make this take a more generic random-access collection source
// if needed, or just make it an array extension instead
func splitAlternating<T>(source: [T]) -> ([T],[T]) {
var evens: [T] = [], odds: [T] = []

evens.reserveCapacity(source.count / 2 + 1)
odds.reserveCapacity(source.count / 2)

for idx in indices(source) {
if idx % 2 == 0 {
evens.append(source[idx])
}
else {
odds.append(source[idx])
}
}

return (evens,odds)
}

let a = [0,1,2,3,4,5,6]
splitAlternating(a) // ([0, 2, 4, 6], [1, 3, 5])

If performance is truly critical, you could use source.withUnsafeBufferPointer to access the source elements, to avoid the index bounds checking.

If the arrays are really huge, and you aren’t going to use the resulting data except to sample a small number of elements, you could consider using a lazy view instead (though the std lib lazy filter isn’t much use here as it returns sequence not a collection – you’d possibly need to write your own).

Swift: Map Array of Objects Alphabetically by Name(String) into Separate Letter Collections within a new Array


let sortedContacts = contactData.sorted(by: { $0.name < $1.name }) // sort the Array first.
print(sortedContacts)

let groupedContacts = sortedContacts.reduce([[Contact]]()) {
guard var last = $0.last else { return [[$1]] }
var collection = $0
if last.first!.name.characters.first == $1.name.characters.first {
last += [$1]
collection[collection.count - 1] = last
} else {
collection += [[$1]]
}
return collection
}
print(groupedContacts)
  1. sort the list. O(nlogn) , where n is the number of items in the Array(contactData).
  2. use reduce to iterate each contact
    in the list, then either add it to new group, or the last one. O(n), where n is the number of items in the Array(sortedContacts).

If you need to have a better printed information, you better make Contact conforms to protocol CustomStringConvertible



Related Topics



Leave a reply



Submit