使用依赖于元素类型的递归属性/方法扩展Collection

时间:2017-01-13 17:34:52

标签: swift generics recursion collections swift-extensions

this question的上下文中,我讨论了如何实现一个属于集合中所有嵌套级别的属性或方法。

直观地说,这应该有用:

extension Collection { 
    var flatCount: Int {
        if self.count == 0 {
            return 0
        } else if self.first is Collection { // .Iterator.Element: Collection
            return self.reduce(0) { (res, elem) -> Int in
                res + (elem as! Collection).flatCount // ERROR
            }
        } else {
            return self.reduce(0) { (res,_) in res + 1 }
        }
    }
}

但是,我们不允许将值转换为具有关联类型的协议类型。

所以我想让Element类型更明确,如下所示:

extension Collection {
    var flatCount: Int {
        return Self.flatCountH(self)
    }

    private static final func 
        flatCountH<C: Collection, D>(_ c: C) -> Int 
            where Iterator.Element == D, D: Collection {
        return c.reduce(0) { (res: Int, elem: D) -> Int in 
            (res + elem.flatCount) as Int // Ambiguous type 
        }
    }

    private static final func flatCountH<C: Collection>(_ c: C) -> Int {
        return c.reduce(0) { $0 + $1.flatCount } // Unable to infer closure type
    }
}

但这显然是对类型推断者提出过多要求。

现在我退后一步,决定停止尝试将所有内容都插入一个扩展名中:

extension Collection {
    var flatCount: Int {
        // There's no count on Collection, so...
        return self.reduce(0) { (res,_) in res + 1 }
    }
}

extension Collection where Iterator.Element: Collection {
    var flatCount: Int {
        return self.reduce(0) { $0 + $1.flatCount }
    }
}

现在这个编译 - 是的! - 但调度已关闭:$1.flatCount不绑定到第二个递归版本,但总是绑定到第一个普通版本。也就是说,flatCount仅计算第一个嵌套级别。

有没有办法以表达此功能的方式处理类型和/或调度?或者我是以一种完全迂回的方式(或两种方式)来实现它?

旁注:在最后一个示例和第一个函数中,我没有使用

self.reduce(0) { $0 + 1 }

因为那不编译;在这里,$0是两个匿名参数的!我认为这是不必要的令人惊讶的行为,并将a change request发布到Swift bugtracker。

2 个答案:

答案 0 :(得分:4)

我不相信它目前可以编写这样的递归扩展,其中基本情况由静态类型的一致性决定。

虽然请注意Collection确实有count属性要求,但它只是类型IndexDistance(关联类型),而不是Int。因此,如果 可能,您可以将其表达为:

extension Collection {
    var flatCount: IndexDistance {
        return count
    }
}

extension Collection where Iterator.Element: Collection {
    var flatCount: IndexDistance {
        // compiler error: unable to infer closure type in the current context
        // (if you expand it out, it will tell you that it's because
        //  $1.flatCount is ambiguous)
        return self.reduce(0) { $0 + $1.flatCount }
    }
}

但是,这会产生编译器错误(尽管为什么flatCountInt时它没有,我不知道 - 它们应该一致编译或不编译)。问题是Swift想要静态调度$1.flatCount - 因此意味着它只能选择一个扩展来调用(在这种情况下,编译器认为两者同样有效)。

静态分派在这里工作的唯一方法是,实现是专门针对他们所要求的每个具体类型的Collection。在这种情况下,歧义将被解决,因为编译器将知道实现中的具体类型,从而知道是否Iterator.Element.Iterator.Element : Collection,并相应地发送。

然而,目前专业化只是一种优化(由于它有可能在不使用内联来抵消这种额外成本的情况下大幅膨胀代码大小) - 因此无法保证静态发送将适用于所有情况。

即使$1.flatCount能够动态调度,例如protocol witness table(请参阅this great WWDC talk),基于扩展的类型约束的重载解析也需要在运行时发生(以确定要调用的扩展名)。但是,Swift在运行时不会解决重载问题(这会很昂贵)。相反,重载本身在编译时被解析,然后动态调度允许该重载的实现相对于它所调用的值是多态的(即它可以调度到一个值拥有的重载实现)。

不幸的是,我认为你可以得到的最接近的是为Array编写扩展并使用条件类型转换来迭代嵌套数组:

extension Array {
    var flatCount: Int {

        var iterator = makeIterator()

        if let first = iterator.next() as? [Any] {
            // must be an array of arrays – otherwise $1 as! [Any] will crash.
            // feel free to add error handling or adding support for heterogeneous arrays
            // by doing an O(n) walk.
            return iterator.reduce(first.flatCount) { $0 + ($1 as! [Any]).flatCount }
        } else {
            return count
        }
    }
}

let arr = [[[[2, 3, 4]], [3, 4, 5, 6]], [57, 89]]

print(arr.flatCount) // 9

虽然在下面的评论中注意到@MartinR points out,但转换as(?/!) [Any]将在大多数情况下创建一个新数组(由于Swift存储具体类型和抽象类型值的方式不同 - 请参阅{ {3}}),使上述实施不是特别有效。

一个可能的解决方案是使用“伪协议”#39;为了声明flatCount属性:

// dummy protocol to prevent conversions of arrays with concrete-typed elements to [Any].
protocol _Array {
    var flatCount: Int { get }
}

extension Array : _Array {
    var flatCount: Int {

        var iterator = makeIterator()

        if let first = iterator.next() as? _Array {
            // same comment as above, can crash for heterogeneous arrays.
            return iterator.reduce(first.flatCount) { $0 + ($1 as! _Array).flatCount }
        } else {
            return count
        }
    }
}

这避免了从具体类型元素数组到抽象类型元素的O(n)转换(相反,只为给定数组创建了一个框)。

如果我们使用数组对两个实现(在MacBook Pro上的发布版本中)进行粗略的快速基准测试:

let arr = Array(repeating: Array(repeating: Array(repeating: 1, count: 100), count: 100), count: 1000)

对于flatCount的10次重复呼叫,第一次分机的时间为31.7秒。适用于第二种实现的相同基准产生0.93秒。

答案 1 :(得分:0)

感觉非常接近:

extension Collection {
    var flatCount: Int {
        return CollectionHelper().flatCountH(self)
    }
}

fileprivate class CollectionHelper {
    func flatCountH<C: Collection>(_ c: C) -> Int where C.Iterator.Element: Collection {
        typealias E = C.Iterator.Element
        return c.reduce(0) { (res: Int, elem: E) -> Int in
            res + self.flatCountH(elem)
        }
    }

    func flatCountH<C: Collection>(_ c: C) -> Int {
        return c.reduce(0) { (count, _) in count + 1 }
    }
}

不幸的是,Swift仍会在这里静态发送。表分派忽略C.Iterator.Element上的类型约束。 不可能声明辅助函数dynamic,因为它们有类型约束 - 太糟糕了。