Функция протокола с общим типом

Я хотел бы создать протокол вроде следующего:

protocol Parser {
    func parse() -> ParserOutcome<?>
}

enum ParserOutcome<Result> {
    case result(Result)
    case parser(Parser)
}

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

Если я использую связанный тип на Parser, тогда я не могу использовать Parser в enum. Если я укажу общий тип функции parse(), тогда я не могу определить его в реализации без общего типа.

Как я могу это достичь?


Используя generics, я мог бы написать что-то вроде этого:

class Parser<Result> {
    func parse() -> ParserOutcome<Result> { ... }
}

enum ParserOutcome<Result> {
    case result(Result)
    case parser(Parser<Result>)
}

Таким образом, параметр Parser будет параметризован по типу результата. parse() может возвращать результат типа Result или любого вида анализатора, который выводит либо результат типа Result, либо другой парсер, параметризованный тем же типом Result.

Вместе со связанными типами, насколько я могу судить, у меня всегда будет ограничение Self:

protocol Parser {
    associatedtype Result

    func parse() -> ParserOutcome<Result, Self>
}

enum ParserOutcome<Result, P: Parser where P.Result == Result> {
    case result(Result)
    case parser(P)
}

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

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

Мне кажется, что определение associatedtype OutcomeParser: Parser внутри протокола Parser, тогда возврат enum, параметризованный этим типом, разрешит проблему, но если я попытаюсь определить OutcomeParser таким образом, я получу ошибку

Тип может не ссылаться как требование

Ответ 1

Состояние функций, необходимых для выполнения этой работы:

  • Ограничения рекурсивного протокола (SE-0157) Реализовано (Swift 4.1)
  • Произвольные требования в протоколах (SE-0142) Реализовано (Swift 4)
  • Псевдонимы типового типа (SE-0048) Реализовано (Swift 3)

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

Когда Swift поддерживает эти две функции, следующее должно стать действительным:

protocol Parser {
    associatedtype Result
    associatedtype SubParser: Parser where SubParser.Result == Result

    func parse() -> ParserOutcome<Result, SubParser>
}

enum ParserOutcome<Result, SubParser: Parser where SubParser.Result == Result> {
    case result(Result)
    case parser(P)
}

С generic typealias es тип subparser также можно извлечь как:

typealias SubParser<Result> = Parser where SubParser.Result == Result

Ответ 2

Я бы не стал так быстро убирать стираемые стили как "хаки" или "работать вокруг [...] системы типов" - на самом деле я бы сказал, что они работают с системой типов, чтобы обеспечить полезный уровень абстракции при работе с протоколами (и, как уже упоминалось, используется в самой стандартной библиотеке, например AnySequence, AnyIndex и AnyCollection).

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

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

Если мы посмотрим на потенциальную реализацию синтаксического анализа типа AnyParser, надеюсь, вы увидите, что я имею в виду:

struct AnyParser<Result> : Parser {

    // A reference to the underlying parser parse() method
    private let _parse : () -> ParserOutcome<Result>

    // Accept any base that conforms to Parser, and has the same Result type
    // as the type erasure generic parameter
    init<T:Parser where T.Result == Result>(_ base:T) {
        _parse = base.parse
    }

    // Forward calls to parse() to the underlying parser method
    func parse() -> ParserOutcome<Result> {
        return _parse()
    }
}

Теперь в вашем ParserOutcome вы можете просто указать, что случай parser имеет связанное значение типа AnyParser<Result> - то есть любой вид реализации синтаксического анализа, который может работать с данным Result общим параметром.

protocol Parser {
    associatedtype Result
    func parse() -> ParserOutcome<Result>
}

enum ParserOutcome<Result> {
    case result(Result)
    case parser(AnyParser<Result>)
}

...

struct BarParser : Parser {
    func parse() -> ParserOutcome<String> {
        return .result("bar")
    }
}

struct FooParser : Parser {
    func parse() -> ParserOutcome<Int> {
        let nextParser = BarParser()

        // error: Cannot convert value of type 'AnyParser<Result>'
        // (aka 'AnyParser<String>') to expected argument type 'AnyParser<_>'
        return .parser(AnyParser(nextParser))
    }
}

let f = FooParser()
let outcome = f.parse()

switch outcome {
case .result(let result):
    print(result)
case .parser(let parser):
    let nextOutcome = parser.parse()
}

В этом примере вы можете видеть, что Swift по-прежнему обеспечивает безопасность типов. Мы пытаемся обернуть экземпляр BarParser (который работает с String s) в стираемой оболочке типа AnyParser, которая ожидает общий параметр Int, приводящий к ошибке компилятора. После FooParser параметрируется работа с String вместо Int, ошибка компилятора будет решена.


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

protocol Parser {
    associatedtype Result
    func parse() -> ParserOutcome<Result>
}

enum ParserOutcome<Result> {
    case result(Result)
    case anotherParse(() -> ParserOutcome<Result>)
}


struct BarParser : Parser {
    func parse() -> ParserOutcome<String> {
        return .result("bar")
    }
}

struct FooParser : Parser {
    func parse() -> ParserOutcome<String> {
        let nextParser = BarParser()
        return .anotherParse(nextParser.parse)
    }
}

...

let f = FooParser()
let outcome = f.parse()

switch outcome {
case .result(let result):
    print(result)
case .anotherParse(let nextParse):
    let nextOutcome = nextParse()
}

Ответ 3

Я думаю, что вы хотите использовать общее ограничение для перечисления ParserOutcome.

enum ParserOutcome<Result, P: Parser where P.Result == Result> {
    case result(Result)
    case parser(P)
}

Таким образом, вы не сможете использовать ParserOutcome все, что не соответствует протоколу Parser. Вы можете добавить еще одно ограничение, чтобы сделать его лучше. Добавление ограничения, что результат результата Parser будет тем же самым типом, что и связанный тип Parser.