Swift:延迟封装地图,过滤器,flatMap链

时间:2019-01-27 08:30:41

标签: swift functional-programming flatmap

我有动物清单:

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] }

略有不同,它们分别类似于filter(输出0或1个元素),map(恰好1个元素)和flatmap(超过1个元素),但是以统一的方式定义,以便可以一致地处理它们。

我想创建一个惰性迭代器,将这些转换的数组应用于动物列表:

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

我很高兴这很简洁,但显然:

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

是一个问题,因为这意味着转换功能仅适用于3个转换数组。

编辑: 我尝试过:

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

但是在标记的行上我得到了错误:

无法将类型为'LazyCollection ,[String] >>>'的值分配为类型为'LazyCollection >'

我了解这是因为每次循环时都会添加另一个平面图,因此类型在变化。

如何使转换函数可以处理任意数量的转换数组?

一种用于有限数量转换的WET解决方案是(但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!")
  }

完整代码:

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

3 个答案:

答案 0 :(得分:7)

如果您根据Sequence协议(而不是Array)定义方法,则可以递归应用转换。另外,如果transformations参数定义为where Element == String的数组,则不需要约束(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...]))
        }
    }
}

答案 1 :(得分:4)

另一种实现您想要的方法:

  

修改:我尝试过:

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

您已经很接近目标了,如果 Error 行中的两种类型都可以分配,那么您的代码就可以了。

一些修改:

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

或者您可以在此处使用reduce

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

整个代码为:

编辑,已修改为包括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))

答案 2 :(得分:2)

如何将其完全纳入功能世界?例如,使用(动态)函数调用链,例如filter(containsA) | map(plural) | flatMap(double)

使用一些可重用的通用代码,我们可以实现一些不错的东西。

让我们从提升一些序列和惰性序列操作到释放功能开始:

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

请注意,惰性序列对应项比常规Sequence更为冗长,但这是由于LazySequenceProtocol方法的冗长性所致。

使用上面的代码,我们可以创建用于接收数组和返回数组的通用函数,并且这种类型的函数非常适合流水线操作,因此让我们定义一个流水线运算符:

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

现在我们需要的是为这些函数提供一些信息,但是要实现这一点,我们需要对Transform类型进行一些调整:

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] }

有了以上所有内容,一切变得简单明了:

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