Расширение Collection с помощью рекурсивного свойства / метода, который зависит от типа элемента

В контексте этого вопроса я подумал о том, как можно реализовать свойство или метод, который учитывает все уровни вложенности в коллекциях.

Интуитивно понятно, что это должно работать:

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 это пара обоих анонимных параметров! Я думаю, что это излишне удивительное поведение, и отправил запрос на изменение в багтрекер Swift.

2 ответа

Решение

Я не верю, что в настоящее время возможно написать рекурсивное расширение, подобное этому, где базовый случай определяется соответствием статического типа.

Хотя учтите что 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 }
    }
}

Тем не менее, это приводит к ошибке компилятора (хотя, почему это не так, когда flatCount является Int Понятия не имею - они оба должны либо последовательно компилироваться, либо не компилироваться). Проблема в том, что Swift хочет статически отправить $1.flatCount - следовательно, это означает, что он может выбрать только одно из расширений для вызова (и в этом случае компилятор считает, что оба одинаково действительны).

Единственный способ статической диспетчеризации мог бы работать здесь, если реализации были специализированы для каждого конкретного типа Collection что они призваны. В этом случае неоднозначность будет разрешена, поскольку компилятор будет знать конкретный тип внутри реализации и, таким образом, будет знать, Iterator.Element.Iterator.Element : Collection и отправить соответственно.

Однако в настоящее время специализация является лишь оптимизацией (поскольку она потенциально может значительно увеличить размер кода без использования встраивания для противодействия этим дополнительным затратам), поэтому невозможно гарантировать, что статическая диспетчеризация будет работать во всех случаях.

Даже если $1.flatCount возможность динамической отправки, например, с помощью таблицы свидетелей протокола (см. этот большой доклад WWDC о них), разрешение перегрузки, основанное на ограничениях типа расширений, должно выполняться во время выполнения (чтобы определить, какое расширение вызывать). Однако 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 в комментариях ниже, преобразование as(?/!) [Any] в большинстве случаев создаст новый массив (из-за разницы в том, как Swift хранит конкретные типизированные и абстрактно-типизированные значения - см. этот раздел вопросов и ответов), что делает приведенную выше реализацию не особенно эффективной.

Одним из возможных решений этой проблемы является использование "фиктивного протокола" для объявления 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)

За 10 повторных звонков flatCount первое расширение дает время 31,7 секунды. Тот же тест, примененный ко второй реализации, дает 0,93 секунды.

Это чувствует себя очень близко:

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 потому что у них есть ограничения типа - тоже плохо.

Другие вопросы по тегам