Swift will not infer opaque return type of generic function

I ran into a situation today where I can't tell if I'm doing something wrong, or if something else is broken. I have the following types, which defines a protocol with an associated type and a function that results in that type. (Much like SwiftUI.View). The function, itself, takes a generic parameter.

import SwiftUI

protocol Style {
    associatedtype Body: View
    @ViewBuilder func makeBody<G>(binding: Binding<G?>) -> Body
}

struct BasicStyle: Style {
    func makeBody<G>(binding: Binding<G?>) -> some View {
        Text("Hello world!")
    }
}

This seems like it should be fine, but the compiler fails with:

error: SwiftUIModifiers.playground:11:8: error: type 'BasicStyle' does not conform to protocol 'Style'
struct BasicStyle: Style {
       ^

SwiftUIModifiers.playground:7:20: note: protocol requires nested type 'Body'; do you want to add it?
    associatedtype Body: View
                   ^

If I remove the generic parameter, G, then it compiles. Is there something that generics does that makes the compiler not able to infer what Body is supposed to be, from the some View keyword?

3 Likes

If I had to guess, I think it's because technically, an opaque return type is dependent on the generic parameters of the function, and therefore can't be an associated type since generic associated types aren't a thing. So for example you can write something like

func someGenericFunction<T>(_ x: T) -> some Any {
    return [x] // concrete type is Array<T>
}

If so, this should probably be fixed so that if an opaque return type can be inferred to be an associated type for a protocol, the return type can't be dependent on the generic parameters of the function.

3 Likes

Thanks for the explanation. That would make a lot of sense. Can you think of any workaround? I am realizing that this is hard to express the relationship that I am trying to express... I could move my generic parameter to be an associated type, but I'm not too keen on that because it makes everything else very messy--and the protocol shouldn't really care what that type is.

protocol Style {
  associatedtype Body
  func makeBody<G>(_: G) -> Body
}

protocol StaticFactory {
  associatedtype Input
  associatedtype Output
  static func make(_ input: Input) -> Output
}

struct BasicStyle: Style {
  enum Helper: StaticFactory {
    // generic-parametrization-free helper
    static func make(_ input: Void) -> some Any {
      42
    }
  }

  func makeBody<G>(_: G) -> Helper.Output {
    Helper.make(())
  }
}

StaticFactory and its implementation Helper is a pattern to give a name to the opaque return type of make function. There was a topic with another issue but the workaround is pretty much identical Out of Line Initialization of Opaque Types - #2 by dmt

3 Likes

Obviously I don't know what the actual implementation is or why a generic parameter is needed, but I think the conventional design of something like this would be to pass the binding in the initializer. That way you don't need a generic parameter.

You mention that would be messy though; so more details on what you're trying to do would help someone come up with a better solution.

2 Likes

@dmt Thank you, that looks very promising.

@adamkuipers I am basically trying to implement something like the SwiftUI *Style types, like PickerStyle or ListStyle. I'm passing in a binding for the selected value, so that I can set up a selector in my custom Picker. I'm trying to emulate the SwiftUI API as much as possible, but it might be more work than it's worth.

But why wouldn't passing the binding in the initializer work for you? It seems if the output type isn't reliant on the input type, it's probably fine that binding is set at initialization

I think that a "style" type should be as much removed from the actual implementation details as possible. If you're making a CustomPicker that can be customized by a more-or-less opaque CustomPickerStyle type, the consumer shouldn't have to care about what the SelectedValue type is. That all is an implementation detail.

In other words, it seems that the actual type of the selection value is unimportant to the style... At most, the style is dealing with moving the data around, but it doesn't matter what the type is. Thus, instead of being defined on the style it would just be a generic type on the method that is dealing with it.

I suspect that what you're trying to do is impossible unless you either stick to some type-erased version of Binding like Binding<Any>, or specify Body explicitly (which means either using AnyView, or messing with SwiftUI's implementation details like _ConditionalContent).

There seems to be a fundamental issue that it's impossible to have a type T that's chosen by the caller (and therefore get a Binding<T>), and then produce an opaque type that's independent of T. There's no way to return an opaque type from a generic function without it being dependent on the function's generic parameters. You also can't work around this by wrapping the Binding<T> in an existential type, because there's no way to "open" an existential and get the T type back, unless you use a generic function like in SE-0352 or a protocol extension, which again means losing the ability to produce an opaque type that's independent of the generic parameters.

If you look at the implementation of ListStyle and PickerStyle in the REPL, you get this:

  1> import SwiftUI
  2> :type lookup ListStyle
Swift Reflection Metadata:
(protocol_composition
  (protocol SwiftUI.ListStyle))
Source code info:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
protocol ListStyle {
  static func _makeView<SelectionValue>(value: SwiftUI._GraphValue<SwiftUI._ListValue<Self, SelectionValue>>, inputs: SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs where SelectionValue : Swift.Hashable
  static func _makeViewList<SelectionValue>(value: SwiftUI._GraphValue<SwiftUI._ListValue<Self, SelectionValue>>, inputs: SwiftUI._ViewListInputs) -> SwiftUI._ViewListOutputs where SelectionValue : Swift.Hashable
}
...
  2> :type lookup PickerStyle
Swift Reflection Metadata:
(protocol_composition
  (protocol SwiftUI.PickerStyle))
Source code info:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
protocol PickerStyle {
  static func _makeView<SelectionValue>(value: SwiftUI._GraphValue<SwiftUI._PickerValue<Self, SelectionValue>>, inputs: SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs where SelectionValue : Swift.Hashable
  static func _makeViewList<SelectionValue>(value: SwiftUI._GraphValue<SwiftUI._PickerValue<Self, SelectionValue>>, inputs: SwiftUI._ViewListInputs) -> SwiftUI._ViewListOutputs where SelectionValue : Swift.Hashable
}
...

So basically, the *Style types in SwiftUI have no associated types at all, and use internal magic to do what they do. With that in mind, in my opinion, you're probably better off implementing your own Style types by returning AnyView or taking a type-erased version of Binding.

I strongly disagree.
While it's true that you can't have an opaque return type proven to be independent from generic parameters, it doesn't prevent you from using a named opaque type.
What you really can't do is to have an associated type that is also an opaque return type of a function that depends from generic parameters of the function. (because the language lacks the support for generic params for associated types)
But this issue also has a workaround, even in the standard library. Take a look at Encoder.container(keyedBy:) https://github.com/apple/swift/blob/main/stdlib/public/core/Codable.swift#L116

1 Like

I'm not aware of any way to make a named opaque type while still retaining a caller-decided type like G. In the example you posted above, the Helper.make function is non-generic, which forces you to use a type-erased Binding, and if it were generic, you'd no longer be able to return an arbitrary SwiftUI view without using type erasure.

The Coding.container(keyedBy:) workaround seems to be using an existential box of some kind, which in my opinion is about the same as using AnyView.

@ellie20 I understand that SwiftUI has no associated type in the *Style types. I had hoped that they had somehow hidden the magic they're using under the hood, but perhaps we'll never know.

I ended up with a nicer workaround that used an enum as my Style type, and then passed in a ViewModifier to the CustomPicker to customize the view in the way I needed instead of through the ViewStyle type.

I would have liked to be able to use an opaque return type with a function that has generic parameters. If Swift had a feature like Kotlin's in/out generics, that might help. Then perhaps you could do a declaration like func make<in G>(_ input: G) -> some View that tells the compiler that the generic parameter will never affect the output of the function.

1 Like

I feel your frustration from why

struct BasicStyle: Style {
    func makeBody<G>(binding: Binding<G?>) -> some View {
        Text("Hello world!")
    }
}

fails to compile. I don't fully understand the reason why.

But when it comes to workaround, would't named concrete type solve your problem?

struct BasicStyle: Style {
    func makeBody<G>(binding: Binding<G?>) -> Text {
        Text("Hello world!")
    }
}

I understand, that you will probably will want to use use multitude of modifiers on Text or otherwise build an "unspellable" result type but can use it behind your named type. You can even pass it the generic binding but you must box it (I'll use just Any for quick demo but you can build your G box that will work for your needs).

struct BasicStyle: Style {

    struct _InferredBody: View {
        @Binding var g: Any
        var body: some View {
            Text("Hello \(String(describing: g))")
                .border(Color.red)
        }
    }

    func makeBody<G>(binding: Binding<G?>) -> _InferredBody {
        _InferredBody(
            g: Binding<Any>(
                get: { binding.wrappedValue as Any },
                set: { binding.wrappedValue = $0 as? G }
            )
        )
    }
}