Swift: Lazily Encapsulating Chains of Map, Filter, Flatmap

Swift: Lazily encapsulating chains of map, filter, flatMap

Another approach to achieve what you want:

Edit: I tried:

var lazyCollection = self.lazy
for transform in transforms {
lazyCollection = lazyCollection.flatMap(transform) //Error
}
var iterator = lazyCollection.makeIterator()

You were very near to your goal, if the both types in the Error line was assignable, your code would have worked.

A little modification:

var lazySequence = AnySequence(self.lazy)
for transform in transforms {
lazySequence = AnySequence(lazySequence.flatMap(transform))
}
var iterator = lazySequence.makeIterator()

Or you can use reduce here:

var transformedSequence = transforms.reduce(AnySequence(self.lazy)) {sequence, transform in
AnySequence(sequence.flatMap(transform))
}
var iterator = transformedSequence.makeIterator()

Whole code would be:

(EDIT Modified to include the suggestions from Martin R.)

let animals = ["bear", "dog", "cat"]

typealias Transform<Element> = (Element) -> [Element]

let containsA: Transform<String> = { $0.contains("a") ? [$0] : [] }
let plural: Transform<String> = { [$0 + "s"] }
let double: Transform<String> = { [$0, $0] }

extension Sequence {
func transform(_ transforms: [Transform<Element>]) -> AnySequence<Element> {
return transforms.reduce(AnySequence(self)) {sequence, transform in
AnySequence(sequence.lazy.flatMap(transform))
}
}
}

let transformed = animals.transform([containsA, plural, double])

print(Array(transformed))

Definition of flatMap in Swift

You were expecting

func flatMap<T>(_ transform: (Element) throws -> Array<T>) rethrows -> Array<T>

for Array.

If you look closely, the above is merely a special case of the signature that you found, when SegmentOfResult == Array<T>.

The flatMap in Array is defined not just for the case when SegmentOfResult == Array<T>, but also for any SegmentOfResult that is a Sequence.

Why? Because we would like to define functions so that they work on as many types as possible (i.e. as general as possible). It turns out that flatMap would also work if the function mapped to any Sequence. That is, it doesn't depend on the fact that the function returns specifically an Array. It only depends on the fact that the function returns some kind of a Sequence. Check out how flatMap is implemented here if you are interested.

For the same reason, flatMap isn't just available on Array, it's available on any type that conforms to Sequence!

How to use a swift Instance method with flatMap?

With instance method, you cannot.

Instance method is a curry method, the type (String) -> () -> Array<String> mean "a method take string and return a function which takes nothing and return array of string".

So you can do like this, but not as you write.

print(animals.flatMap{ String.double($0)() }) // ["Ant", "Ant", "Bear", "Bear", "Cat", "Cat"]

What you need is a static method. It simply take string and return array of string.

extension String {
static func double(_ string: String) -> [String] {
return [string, string]
}
}

print(animals.flatMap(String.double)) // ["Ant", "Ant", "Bear", "Bear", "Cat", "Cat"]

Swift writing map, length, filter as reduce

For a mapping which transforms a type T into a (possibly different) type S
simply use two type placeholders:

func map<T, S>(array: [T], f: (T->S)) -> [S] {
return array.reduce([]) {
(var seed, value) in
seed.append(f(value))
return seed
}
}

Example:

let x = map([1, 2, 3], f: { String($0) })
print(x) // ["1", "2", "3"]

Whether this "is this the most elegant syntax" is also a
matter of personal opinion. If you replace the append() method
by array concatenation with + then the seed parameters needs
not be variable:

func map<T, S>(array: [T], f: (T->S)) -> [S] {
return array.reduce([]) {
(seed, value) in
seed + [f(value)]
}
}

Which can be written with shorthand parameter names as

func map<T, S>(array: [T], f: (T->S)) -> [S] {
return array.reduce([]) { $0 + [ f($1) ] }
}

Similarly:

func filter<T>(array: [T], predicate: (T->Bool)) -> [T]{
return array.reduce([]) { predicate($1) ? $0 + [ $1 ] : $0 }
}

Just note that an implementation of map() and filter() using
reduce() (instead of a loop) is quite ineffective because a new array is created in each reduction step. See for example

  • Arrays, Linked Lists and Performance

for an analysis. For an exercise (as you said) it is fine, just
don't use something like this in production).

Using flatMap with dictionaries produces tuples?

I see that your question boils down to map vs. flatMap. map is the more consistent one and it works as expected here so I won't delve into it.

Now on to flatMap: your problem is one of the reasons that SE-0187 was proposed. flatMap has 3 overloads:

Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element] where S : Sequence
Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]
  • When your dictionary() function returns a non-optional, it uses the first overload. Since a dictionary is a sequence of key-value tuples, an array of tuple is what you get.

  • When your dictionary() function returns an optional, it uses the third overload, which essentially filters out the nils. The situation can be very confusing so SE-0187 was proposed (and accepted) to rename this overload to compactMap.

Starting from Swift 4.1 (Xcode 9.3, currently in beta), you can use compactMap:

// Assuming dictionary() returns [String: Any]?
dictionary["objects"] = objects.compactMap { $0.dictionary() }

For Swift < 4.1, the solution you provided is the only one that works, because it gives the compiler a hint to go with the third overload:

dictionary["objects"] = objects.flatMap { $0.dictionary() } as [[String: Any]]


Related Topics



Leave a reply



Submit