Тип Erasure для более сложных протоколов - PullRequest
0 голосов
/ 03 декабря 2018

Я строю декларативную и основанную на типе модель фильтра.Я застрял, сохраняя состояние активных фильтров в свойстве, потому что мои протоколы имеют связанные типы .`

Я слышал о стирании типов , но все примеры, которые я нашел, где использовались суперпростые примеры, и почему-то я не могу сопоставить его с моим вариантом использования.

Это мой протокол:

protocol Filter {
    // The Type to be filtered (`MyModel`)
    associatedtype ParentType
    // The type of the property to be filtered (e.g `Date`)
    associatedtype InputType
    // The type of the possible FilterOption (e.g. `DateFilterOption` or the same as the Input type for filtering in enums.)
    associatedtype OptionType        
    // This should return a list of all possible filter options
    static var allOptions: [OptionType] { get }

    static var allowsMultipleSelection: Bool { get }        
    // the adopting object will be setting this.
    var selectedOptions: [OptionType] { get set }        

    func isIncluded(_ item: InputType) -> Bool        
    // For getting reference to the specific property. I think Swift 4's keypaths could be working here too.
    var filter: FilterClosure<ParentType> { get }
}

И суб-протоколы, которые имеют расширения для сокращения кода копирования / вставки

protocol EquatableFilter: Filter where InputType: Equatable, OptionType == InputType {}
extension EquatableFilter {
    var allowsMultipleSelection: Bool { return true }
    func isIncluded(_ item: InputType) -> Bool {
        if selectedOptions.count == 0 { return true }
        return selectedOptions.contains(item)
    }
}

// Another specific filter. See gist file for extension.
protocol DateFilter: Filter where InputType == Date, OptionType == DateFilterOption {}

Для получения дополнительной информации, см. Мою суть , чтобы увидеть, как выглядит моя реализация, с примером модели.


Вопросы

  1. Как сохранить массив, содержащий struct экземпляров, соответствующих различным Filter протоколам?

  2. И как я могу хранить статическиемассив, содержащий только типы структур, поэтому я могу получить доступ к статическим свойствам?

1 Ответ

0 голосов
/ 04 декабря 2018

Интересно, что в начале этого года я построил нечто похожее на коммерческий проект.Это сложно сделать в общем, но большинство проблем возникает из-за того, что мы думаем задом наперед.«Начните с конца».

// I want to be able to filter a sequence like this:
let newArray = myArray.filteredBy([
    MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]),
    MyModel.Filters.StatusFilter(selectedOptions: [.a, .b])
    ])

Эта часть очень проста.Это даже не требует filteredBy.Просто добавьте .filter к каждому элементу:

let newArray = myArray
    .filter(MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]).filter)
    .filter(MyModel.Filters.StatusFilter(selectedOptions: [.a, .b]).filter)

Если вы хотите, вы можете написать фильтрованный таким образом и сделать то же самое:

func filteredBy(_ filters: [(Element) -> Bool]) -> [Element] {...}

Дело в том, чтоFilter здесь на самом деле не «фильтр».Это описание фильтра, с множеством других вещей, связанных с пользовательским интерфейсом (мы поговорим об этом позже).Чтобы на самом деле фильтровать, все, что вам нужно, это (Element) -> Bool.

Что нам действительно нужно, так это способ создать ([Element]) -> Element с хорошим, выразительным синтаксисом.На функциональном языке это было бы довольно просто, потому что у нас были бы такие вещи, как частичное применение и функциональная композиция.Но Свифт на самом деле не любит делать такие вещи, поэтому, чтобы сделать его красивее, давайте построим некоторые структуры.

struct Filter<Element> {
    let isIncluded: (Element) -> Bool
}

struct Map<Input, Output> {
    let transform: (Input) -> Output
}

Нам понадобится способ начать, поэтому давайте воспользуемся картой идентичности

extension Map where Input == Output {
    init(on: Input.Type) { transform = { $0 }}
}

И нам нужен способ подумать о keyPaths

extension Map {
    func keyPath<ChildOutput>(_ keyPath: KeyPath<Input, ChildOutput>) -> Map<Input, ChildOutput> {
        return Map<Input, ChildOutput>(transform: { $0[keyPath: keyPath] })
    }
}

И, наконец, мы хотим создать реальный фильтр

extension Map {
    func inRange<RE: RangeExpression>(_ range: RE) -> Filter<Input> where RE.Bound == Output {
        let transform = self.transform
        return Filter(isIncluded: { range.contains(transform($0)) })
    }
}

Добавить помощника для«последние 24 часа»

extension Range where Bound == Date {
    static var last24Hours: Range<Date> { return Date(timeIntervalSinceNow: -24*60*60)..<Date() }
}

И теперь мы можем создать фильтр, который выглядит следующим образом:

let filters = [Map(on: MyModel.self).keyPath(\.dueDate).inRange(Range.last24Hours)]

filters имеет тип Filter<MyModel>, так что любая другая вещь, которая фильтруетMyModel является законным здесь.Настройка filteredBy:

extension Sequence {
    func filteredBy(_ filters: [Filter<Element>]) -> [Element] {
        return filter{ element in filters.allSatisfy{ $0.isIncluded(element) } }
    }
}

OK, это шаг фильтрации.Но ваша проблема также в основном в «конфигурации пользовательского интерфейса», и для этого вы хотите захватить гораздо больше элементов, чем это делает.

Но ваш пример использования не поможет вам:

// Also I want to be able to save the state of all filters like this
var activeFilters: [AnyFilter] = [ // ???
    MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]),
    MyModel.Filters.StatusFilter(selectedOptions: [.a, .b])
]

Как вы можете преобразовать AnyFilter в элементы пользовательского интерфейса?Ваш протокол фильтра позволяет буквально любой тип опции.Как бы вы отобразили пользовательский интерфейс для этого, если бы тип опции был OutputStream или DispatchQueue?Созданный вами тип не решает проблему.

Вот один из способов решения этой проблемы.Создайте структуру FilterComponent, которая определяет необходимые элементы пользовательского интерфейса и предоставляет способ создания фильтра.

struct FilterComponent<Model> {
    let optionTitles: [String]
    let allowsMultipleSelection: Bool
    var selectedOptions: IndexSet
    let makeFilter: (IndexSet) -> Filter<Model>
}

Затем для создания компонента фильтра даты нам понадобятся некоторые параметры для дат.

enum DateOptions: String, CaseIterable {
    case inPast24hours = "In the past 24 hours"
    case inNext24hours = "In the next 24 hours"

    var dateRange: Range<Date> {
        switch self {
        case .inPast24hours: return Date(timeIntervalSinceNow: -24*60*60)..<Date()
        case .inNext24hours: return Date()..<Date(timeIntervalSinceNow: -24*60*60)
        }
    }
}

И затем мы хотим создать такой компонент с правильным makeFilter:

extension FilterComponent {
    static func byDate(ofField keyPath: KeyPath<Model, Date>) -> FilterComponent<Model> {
        return FilterComponent(optionTitles: DateOptions.allCases.map{ $0.rawValue },
                               allowsMultipleSelection: false,
                               selectedOptions: [],
                               makeFilter: { indexSet in
                                guard let index = indexSet.first else {
                                    return Filter<Model> { _ in true }
                                }
                                let range = DateOptions.allCases[index].dateRange
                                return Map(on: Model.self).keyPath(keyPath).inRange(range)
        })
    }
}

. При этом мы можем создавать компоненты типа FilterComponent<MyModel>.Никакие внутренние типы (такие как Date) не должны быть выставлены.Протоколы не нужны.

let components = [FilterComponent.byDate(ofField: \MyModel.dueDate)]
...