Weird behaviour when using generic functions in protocols and opaque types

I'm trying to figure out why this happens:

protocol WidgetRepresentable {
    associatedtype W: Widget
    func widgetFor<Value>(keypath: KeyPath<Self, Value>) -> W
}

// 1. Then, this works
struct SomeModel: WidgetRepresentable {
    func widgetFor<Value>(keypath: KeyPath<Self, Value>) -> SliderWidget<Float> {
        SliderWidget(from: 0, to: 1)
    }
}

// 2. While this doesn't
// error: Type 'SomeModel' does not conform to protocol 'WidgetRepresentable'
struct SomeModel: WidgetRepresentable {
    func widgetFor<Value>(keypath: KeyPath<Self, Value>) -> some Widget {
        SliderWidget(from: 0, to: 1)
    }
}

Yet, remove the generic parameter and everything works again:

protocol WidgetRepresentable {
    associatedtype W: Widget
    func widgetWith(name: String) -> W
}

struct SomeModel: WidgetRepresentable {
    func widgetWith(name: String) -> some Widget {
        SliderWidget(from: 0, to: 1)
    }
}

It's like you either have a generic parameter or an opaque return value, but never both. However, some Widget should be resolved to SliderWidget before deciding the return type in the second case, which means it should work as the first case does (?).

Yup, so the feature you're relying on is called associated type inference, which is pretty convenient when it works. However:

Our current system for associated type inference and associated type defaults is buggy and complicated.

A proposal to remove the feature outright was rejected, and other attempts to rework the feature haven't really panned out.

Here, you're encountering the issue that associated type inference can't infer typealias W = some Widget [result of widgetFor(keypath:)] based on your implementation of a generic function requirement. Meanwhile, you can't actually spell out result of widgetFor(keypath:) explicitly as a typealias. This is indeed a limitation of Swift today.

5 Likes

@xwu Thanks for the reply. Really helpful.

That's unfortunate, I feel like these abstractions would have been really nice to have for my usecase. As far as I can tell, there's no workaround to this either. As you mentioned, partly because of the associated type inference failure and partly because one can't explicitly use a typealias using an opaque type (for obvious reasons).