Swift: Lazily encapsulating chains of map, filter, flatMap

You can apply the transformations recursively if you define the method on the Sequence protocol (instead of Array). Also the constraint where Element == String is not needed if the transformations parameter is defined as an array of (Element) -> [Element].

extension Sequence {
    func transform(_ transforms: [(Element) -> [Element]]) -> AnySequence<Element> {
        if transforms.isEmpty {
            return AnySequence(self)
        } else {
            return lazy.flatMap(transforms[0]).transform(Array(transforms[1...]))
        }
    }
}

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))

How about fully taking this into the functional world? For example using (dynamic) chains of function calls, like filter(containsA) | map(plural) | flatMap(double).

With a little bit of reusable generic code we can achieve some nice stuff.

Let's start with promoting some sequence and lazy sequence operations to free functions:

func lazy<S: Sequence>(_ arr: S) -> LazySequence<S> {
    return arr.lazy
}

func filter<S: Sequence>(_ isIncluded: @escaping (S.Element) throws -> Bool) -> (S) throws -> [S.Element] {
    return { try $0.filter(isIncluded) }
}

func filter<L: LazySequenceProtocol>(_ isIncluded: @escaping (L.Elements.Element) -> Bool) -> (L) -> LazyFilterSequence<L.Elements> {
    return { $0.filter(isIncluded) }
}

func map<S: Sequence, T>(_ transform: @escaping (S.Element) throws -> T) -> (S) throws -> [T] {
    return { try $0.map(transform) }
}

func map<L: LazySequenceProtocol, T>(_ transform: @escaping (L.Elements.Element) -> T) -> (L) -> LazyMapSequence<L.Elements, T> {
    return { $0.map(transform) }
}

func flatMap<S: Sequence, T: Sequence>(_ transform: @escaping (S.Element) throws -> T) -> (S) throws -> [T.Element] {
    return { try $0.flatMap(transform) }
}

func flatMap<L: LazySequenceProtocol, S: Sequence>(_ transform: @escaping (L.Elements.Element) -> S) -> (L) -> LazySequence<FlattenSequence<LazyMapSequence<L.Elements, S>>> {
    return { $0.flatMap(transform) }
}

Note that the lazy sequences counterparts are more verbose that the regular Sequence ones, but this is due to the verbosity of LazySequenceProtocol methods.

With the above we can create generic functions that receive arrays and return arrays, and this type of functions are extremely fitted for pipelining, so let's define a pipeline operator:

func |<T, U>(_ arg: T, _ f: (T) -> U) -> U {
    return f(arg)
}

Now all we need is to feed something to these functions, but to achieve this we'll need a little bit of tweaking over the Transform type:

typealias Transform<T, U> = (T) -> U

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

With all the above in place, things get easy and clear:

let animals = ["bear", "dog", "cat"]
let newAnimals = lazy(animals) | filter(containsA) | map(plural) | flatMap(double)
print(Array(newAnimals)) // ["bears", "bears", "cats", "cats"]