Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Lazily encapsulating chains of map, filter, flatMap

I have a list of animals:

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

And some ways to transform that list:

typealias Transform = (String) -> [String]

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

As a slight aside, these are analogous to filter (outputs 0 or 1 element), map (exactly 1 element) and flatmap (more than 1 element) respectively but defined in a uniform way so that they can be handled consistently.

I want to create a lazy iterator which applies an array of these transforms to the list of animals:

extension Array where Element == String {
  func transform(_ transforms: [Transform]) -> AnySequence<String> {

    return AnySequence<String> { () -> AnyIterator<String> in
      var iterator = self
        .lazy
        .flatMap(transforms[0])
        .flatMap(transforms[1])
        .flatMap(transforms[2])
        .makeIterator()

      return AnyIterator {
        return iterator.next()
      }
    }
  }
}

which means I can lazily do:

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

and to check the result:

print(Array(transformed))

I'm pleased with how succinct this is but clearly:

        .flatMap(transforms[0])
        .flatMap(transforms[1])
        .flatMap(transforms[2])

is an issue as it means the transform function will only work with an array of 3 transforms.

Edit: I tried:

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

but on the marked row I get error:

Cannot assign value of type 'LazyCollection< FlattenCollection< LazyMapCollection< Array< String>, [String]>>>' to type 'LazyCollection< Array< String>>'

which I understand because each time around the loop another flatmap is being added, so the type is changing.

How can I make the transform function work with an array of any number of transforms?

One WET solution for a limited number of transforms would be (but YUK!)

  switch transforms.count {
  case 1:
    var iterator = self
      .lazy
      .flatMap(transforms[0])
      .makeIterator()
    return AnyIterator {
      return iterator.next()
    }
  case 2:
    var iterator = self
      .lazy
      .flatMap(transforms[0])
      .flatMap(transforms[1])
      .makeIterator()
    return AnyIterator {
      return iterator.next()
    }
  case 3:
    var iterator = self
      .lazy
      .flatMap(transforms[0])
      .flatMap(transforms[1])
      .flatMap(transforms[2])
      .makeIterator()
    return AnyIterator {
      return iterator.next()
    }
  default:
    fatalError(" Too many transforms!")
  }

Whole code:

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

typealias Transform = (String) -> [String]

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

extension Array where Element == String {
  func transform(_ transforms: [Transform]) -> AnySequence<String> {

    return AnySequence<String> { () -> AnyIterator<String> in
      var iterator = self
        .lazy
        .flatMap(transforms[0])
        .flatMap(transforms[1])
        .flatMap(transforms[2])
        .makeIterator()

      return AnyIterator {
        return iterator.next()
      }
    }
  }
}

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

print(Array(transformed))
like image 919
Cortado-J Avatar asked Jan 27 '19 08:01

Cortado-J


1 Answers

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...]))
        }
    }
}
like image 155
Martin R Avatar answered Nov 11 '22 10:11

Martin R