具有通用类型的协议函数

14

我想创建一个类似以下的协议:

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

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

我希望有解析器可以返回指定类型的结果或者另一个解析器。

如果在Parser上使用关联类型,那么就不能在enum中使用Parser。如果我在parse()函数上指定一个泛型类型,那么在实现中我将无法定义它而不带有泛型类型。

我该如何实现这个需求?


使用泛型,我可以编写以下内容:

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协议获得与通用定义相同的行为,并且我想在类型系统范围内做到这一点,而不引入新的装箱类型,就像我使用普通泛型定义一样。

对我来说,似乎在Parser协议中定义associatedtype OutcomeParser: Parser,然后返回由该类型参数化的enum可以解决问题,但如果我尝试以那种方式定义OutcomeParser,就会出现错误:

Type may not reference itself as a requirement


我自己无法为此编写答案,但在我看来,您可能正在寻找类型擦除。我知道@RobNapier在他的一些答案中展示了优雅的使用方式,也许您可以在那里找到一些要探究的东西。 - dfrib
1
嗯,确实,AnySequence 使用了类型擦除,它在标准库中,并且被苹果明确地文档化为“类型擦除”。到目前为止,它仍然感觉像是一种hack,但我正在深入研究它。 - rid
1
类型擦除并不是一种黑客行为。它们在 Swift 标准库中被广泛使用。您可以在此处阅读更多信息 - https://www.natashatherobot.com/swift-type-erasure/. - itskoBits
@pyon,另外,我想使用组合而不是继承。如果我要有一个“类”,那么我将被迫对其进行子类化,这是我不想做的事情,特别是因为在Swift中没有“抽象”类。 - rid
@rid:啊,非常有道理。在我看来,您想通过强制其关联的“Result”类型与其他地方定义的另一种类型(在本例中为“ParserOutcome”的“Result”)相一致,来定义您的“Parser”协议的子类型(“子协议”?)。但是据我所知,在Swift中这是不可能的。不过我很乐意错。 - isekaijin
显示剩余4条评论
3个回答

6
我不会轻易将类型擦除视为“hacky”或“绕过[...]类型系统”的做法 - 实际上,我认为它们是与类型系统一起工作的方式,以便在使用协议时提供有用的抽象层(正如已经提到的那样,标准库中也使用了这种方法,例如AnySequenceAnyIndexAnyCollection)。
正如您自己所说,您想要做的就是有可能从解析器返回给定结果,或者另一个使用相同结果类型的解析器。我们不关心该解析器的具体实现方式,我们只想知道它是否具有一个返回相同类型结果或具有同样要求的另一个解析器的parse()方法。
类型擦除非常适合这种情况,因为您只需要引用给定解析器的parse()方法,就可以将其余部分的实现细节抽象化。值得注意的是,在这里您不会失去任何类型安全性,您正是按照要求精确指定解析器的类型。
如果我们看一个潜在的类型擦除解析器AnyParser的实现,希望您能理解我的意思:
struct AnyParser<Result> : Parser {

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

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

    // Forward calls to parse() to the underlying parser's 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仍然强制类型安全。我们试图将一个使用String的BarParser实例包装在期望Int泛型参数的AnyParser类型擦除包装器中,从而导致编译器错误。一旦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()
}

不错!在你的第二个解决方案中,为什么我们不需要通过“间接”标记parser case?当一个case的关联值是具有与枚举相同的_return_类型的闭包时,它不是递归枚举吗? - dfrib
@dfri 哦,有趣,我甚至没有想到这一点。我猜这是因为函数是引用类型 - 因此不需要额外的间接层来存储枚举中的 () -> ParserOutcome<Result> 函数,因为存储引用的内存块具有已知的大小,并且不依赖于枚举本身。 - Hamish
1
感谢您详细的解释。我确实理解了Swift版本类型擦除背后的思想,也明白了您不会失去类型安全性,但是我所说的“hack”的意思是,您不应该需要做所有这些事情来表达您的要求。Swift团队自己也认识到了这一点,并在谈论关于AnySequence类型的递归协议约束时表示了这一点。就像我一样,他们不喜欢AnySequence的现状,并计划通过引入递归协议约束来修复它。 - rid
@dfri,确实,这是一个尚未实现的功能。如果Swift 4实际上实现了它,我不会感到惊讶,因为它似乎是优先事项清单中的重点(实际上是完整泛型宣言中的第一部分)。 - rid
@rid 我完全同意内置语言功能允许您表达这种要求的想法很不错 - 尽管您应该注意在此处使用类型擦除和递归协议关联类型约束之间存在微妙的区别,后者将限制SubParser符合Parser。使用关联类型,Parser的实现将负责指定SubParser的具体具体类型,而不是接受“处理相同结果的任何解析器”。 - Hamish
显示剩余6条评论

3

使其正常工作所需的功能状态:

  • 递归协议限制(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)
}

有了 通用的 typealias, 子解析器类型也可以被提取为:

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

我之前提到了类型擦除,但也许你可以适应递归枚举,以帮助你顺利完成(关键字indirect case)。 - dfrib
@dfri,是的,我编辑了问题,提到我想在类型系统的范围内完成这个操作,而不引入新的装箱类型,就像我可以使用普通泛型定义一样。我考虑过“间接”枚举,但我认为在我的情况下这不起作用,因为“解析器”本身不是枚举,而是需要包含逻辑的东西。 - rid
是的,我尝试过使用递归枚举来构建像上面那样的解析器,但是没有成功(我认为我应该加上这一点,以防其他人可以更有创意地利用这个结构)。 - dfrib

0

我认为你想在ParserOutcome枚举上使用通用约束。

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

这样做的话,您将无法使用与未符合Parser协议的任何内容相关的ParserOutcome。实际上您可以添加一个限制以使其更好。添加约束条件,即Parser结果的结果与Parser关联类型相同。

没错,谢谢。但 parse() 的返回类型问题仍然存在:我该如何定义它以便接受任何类型的 Parser,而不仅仅是 Self - rid
1
哦,我现在明白了。目前的Swift版本不允许递归协议约束。我想这个问题已经在Swift进化邮件列表中讨论过,但不会包含在Swift 3.0发布版中。我认为现在最好的解决方法可能是类型擦除。 - itskoBits
事实上,现在看来,类型擦除似乎非常优雅,但它绝对是一种技巧(即使它是官方的),因为它正在解决类型系统中的限制。如果协议可以是通用的,那将是很好的,但在此之前(或者直到递归引用被实现),将类型装箱到类型擦除容器中似乎是必要的解决方法。 - rid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接