Функция протокола с общим типом
Я хотел бы создать протокол вроде следующего:
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
.