如何创建懒惰组合

时间:2017-07-16 20:09:33

标签: swift functional-programming lazy-sequences

我的问题非常简单,如何让这段代码变得懒惰:

/*
input: [
    [1, 2],
    [3, 4],
    [5, 6]
]

output: [
    [1, 3, 5],
    [1, 3, 6],
    [1, 4, 5],
    [1, 4, 6],
    [2, 3, 5],
    [2, 3, 6],
    [2, 4, 5],
    [2, 4, 6],
]
*/

func combinations<T>(options: [[T]]) -> [[T]] {
    guard let head = options.first else {
        return [].map({ [$0] })
    }

    if options.count == 1 {
        return head.map({ [$0] })
    }

    let tailCombinations = combinations(options: Array(options.dropFirst()))

    return head.flatMap({ option in
        return tailCombinations.map({ combination -> [T] in
            return [option] + combination
        })
    })
}

上面的代码用于计算组合,但它确实在内存中创建了整个数组。 我需要的是让它返回类似LazySequence<Array<T>>的东西,除了Swift类型系统不允许我做一些通用的东西。

任何想法如何实现这一点并保持功能风格?

Ps。:我确实想到了用生成器解决这个问题并跟踪索引的另一种方法,但我不想跟踪任何状态,我想要一个纯函数(如FP)解决方案。 Haskell默认做到了,顺便说一下,我正在寻找同样的东西。

编辑:我设法通过AnyCollection

解决部分问题,类型系统
func combinations<T>(options: [[T]]) -> LazyCollection<AnyCollection<[T]>> {
    guard let head = options.first else {
        return AnyCollection([].lazy.map({ [$0] })).lazy
    }

    if options.count == 1 {
        return AnyCollection(head.lazy.map({ [$0] })).lazy
    }

    let tailCombinations = combinations(options: Array(options.dropFirst()))

    return AnyCollection(head.lazy.flatMap({ option in
        return tailCombinations.lazy.map({ [option] + $0 })
    })).lazy
}

但是当我使用该函数时,它会将整个集合加载到内存中,即不是懒惰的。

编辑2:进行更多调查,结果发现问题出在AnyCollection

// stays lazy
let x1 = head.lazy.flatMap({ option in
    return tailCombinations.lazy.map({ [option] + $0 })
})

// forces to load in memory
let x2 = AnyCollection(head.lazy.flatMap({ option in
    return tailCombinations.lazy.map({ [option] + $0 })
}))

不知道如何解决这个问题。

2 个答案:

答案 0 :(得分:3)

以下是我提出的建议:

func combinations<T>(options: [[T]]) -> AnySequence<[T]> {
    guard let lastOption = options.last else {
        return AnySequence(CollectionOfOne([]))
    }
    let headCombinations = combinations(options: Array(options.dropLast()))
    return AnySequence(headCombinations.lazy.flatMap { head in
        lastOption.lazy.map { head + [$0] }
    })
}

this solution的主要区别在于递归 call创建一个序列 第一个 N-1选项,然后组合每个元素 该序列与最后一个选项的每个元素。这是更多 高效,因为递归调用返回的序列 仅枚举一次,而不是每个元素一次枚举 结合。

其他差异是:

  • 如果有.lazy,则无需致电AnySequence 序列已经很懒惰了。因此返回类型“简化” 到AnySequence<[T]>
  • 我使用CollectionOfOne创建单元素序列 对于空阵列。
  • 不需要单独处理案例options.count == 1 使算法工作(但可能是一种可能的性能 改善)。

完全不同的方法是定义自定义集合类型 使用,计算每个组合作为索引的函数 简单的模运算:

struct Combinations<T> : RandomAccessCollection {
    let options: [[T]]
    let startIndex = 0
    let endIndex: Int

    init(options: [[T]]) {
        self.options = options.reversed()
        self.endIndex = options.reduce(1) { $0 * $1.count }
    }

    subscript(index: Int) -> [T] {
        var i = index
        var combination: [T] = []
        combination.reserveCapacity(options.count)
        options.forEach { option in
            combination.append(option[i % option.count])
            i /= option.count
        }
        return combination.reversed()
    }
}

不需要额外的存储空间,也不需要递归。用法示例:

let all = Combinations(options: [[1, 2], [3, 4], [5, 6]])
print(all.count)
for c in all { print(c) }

输出:

8
[1, 3, 5]
[1, 3, 6]
[1, 4, 5]
[1, 4, 6]
[2, 3, 5]
[2, 3, 6]
[2, 4, 5]
[2, 4, 6]

使用

进行测试
let options = Array(repeating: [1, 2, 3, 4, 5], count: 5)

这个基于集合的方法比这更快 我上面基于序列的方法是因子2。

答案 1 :(得分:1)

我找到了一个可能的解决方案,但我会暂时不接受这个答案,看看是否有人知道更好的答案。

func combinations<T>(options: [[T]]) -> LazySequence<AnySequence<[T]>> {
    guard let head = options.first else {
        return AnySequence([].lazy.map({ [$0] })).lazy
    }

    if options.count == 1 {
        return AnySequence(head.lazy.map({ [$0] })).lazy
    }

    let tailCombinations = combinations(options: Array(options.dropFirst()))

    return AnySequence(head.lazy.flatMap({ option in
        return tailCombinations.lazy.map({ [option] + $0 })
    })).lazy
}

解决方案是使用AnySequence代替AnyCollection。 我不确定为什么,我仍然希望使用AnyCollection界面而不是AnySequence,因为它为我提供了更多方法,例如count