Swift Protocol Generic as Function Return Type

Swift protocol generic as function return type

The problem is you cannot use the syntax P<T>. P is a protocol, meaning it can't be treated as a generic type (Cannot specialize non-generic type 'P'), even though it may have a given associatedtype.

In fact, because it has an associatedtype, you now can't even use the protocol type itself directly – you can only use it as a generic constraint.

One solution to your problem is to simply change your function signature to createC<T>() -> C<T>, as that's exactly what it returns.

class Factory {
func createC<T>() -> C<T> {
return C<T>()
}
}

I'm not entirely sure what you would gain from having the return type be a protocol here. Presumably your example is just a simplification of your actual code and you want to be able to return an arbitrary instance that conforms to P. In that case, you could use type erasure:

class AnyP<T> : P {

private let _get : () -> T?
private let _set : (T) -> ()

init<U:P where U.T == T>(_ base:U) {
_get = base.get
_set = base.set
}

func get() -> T? {return _get()}
func set(v: T) {_set(v)}
}

class Factory {
func createC<T>() -> AnyP<T> {
return AnyP(C<T>())
}
}

How to make return type of function generic in Swift

This isn't possible.

What generics are for

Suppose you have this function:

func identity(_ value: Any) -> Any {
return value
}

It doesn't actually work:

let i = 5
assert(identity(i) == i) // ❌ binary operator '==' cannot be applied to operands of type 'Any' and 'Int'

Any causes type information to be lost. Even though we see that the type of the parameter and the return value will always be the same, we haven't expressed that to the type system. This is a perfect use-case for a generic type parameter. It allows us to express the relationships between the types of the parameter and the return value.

func identity<T>(_ value: T) -> T {
return value
}

let i = 5
assert(identity(i) == i) // ✅

What generics are not for

Looking back at your problem, you see that there are no type relationships here to be expressed.

  • ClassA.validateModel() always returns [String]
  • ClassB.validateModel() always returns [Int]
  • ClassC.validateModel() always returns [MyCustomEnum]

That's not generic.

How would it even work?

Suppose you had an object of type ElementData. That object could be an instance of ElementData, or of ClassA, or ClassB, or ClassC. Given that all four of these types are possible, and assuming there exists some concoction to do what you want, how would this code work?

let elementData = someElementData()
let validatedModel = elementData.validateModel() // What type is `someValue` supposed to be?

Since we (nor the compiler) knows what concrete type the value of elementData will be (we only know that it's an ElementData, or one of its subclasses), how is the compiler supposed to determine the type of validatedModel ?

Furthermore, your code would be breaking the Liskov Substitution Principle. ClassA needs to support being substituted where ElementData is expected. One of the things that ElementData.validateModel()can do is return a Something. Thus, ClassA.validateModel() needs to either return a Something, or a subclass (strangely, it seems that only inheritence relationships work, not protocol subtype relationships. For example, returning Int where Any is expected doesn't work). Since ClassA.validateModel() returns Array<String>, and Array isn't a class (thus, can't have a superclass), there's no possible type Something that could be used to make the code not violate LSP and compile.

Here's an illustration of LSP, and how Covariance works in return types of overridden methods, and not in parameter types of overridden methods.

// https://www.mikeash.com/pyblog/friday-qa-2015-11-20-covariance-and-contravariance.html

class Animal {}
class Cat: Animal {}

class Person {
func purchaseAnimal() -> Animal {
return Animal()
}
}

class CrazyCatLady: Person {
// Totally legal. `Person` has to be able to return an `Animal`.
// A `Cat` is an animal, so returning a `Cat` where an `Animal` is required is totally valid
override func purchaseAnimal() -> Cat {
return Cat()
}

// This method definition wouldn't be legal, because it violates the Liskov Substitution Principle (LSP).
// A `CrazyCatLady` needs to be able to stand in anywhere a `Person` can be used. One of the things a
// `Person` can do is to `pet(animal: Animal)`. But a `CrazyCatLady` can't, because she can only pet cats.
//
// If this were allowed to compile, this could would be undefined behaviour:
//
// let person: Person = getAPerson()
// let animal: Animal = getAnAnimal()
// person.pet(animal)
//
// override func pet(animal: Cat) { // ❌ method does not override any method from its superclass
//
// }
}

One approach to a solution

First of all, we need to establish what's in common between these return types. If we could do that, then the compiler has an answer to the question of "what type should someModel be?" above.

There are two tools available:

  1. Class inheritance (subclasses are subtypes of their superclasses)
  2. Protocol conformance (protocol conforming types are subtypes of the protocols they conform to)

Both have advantages/disadvantages. Protocols force you in the painful road of dispair that is associated-type, whereas Classes are less flexible (since they can't be subclassed by enums or structs). In this case, the answer lies with what you want this code to do. Fundamentally, you're trying to hook this data up to a table cell. So make a protocol for that:

protocol CellViewDataSource {
func populate(cellView: UICellView) {
// adjust the cell as necessary.
}
}

Now, update your methods to return this type:

class ElementData {
func validateModel() -> CellViewDataSource {
fatalError()
}
}

class ClassA {
func validateModel() -> CellViewDataSource {
fatalError()
}
}

To implement these methods, you'll have to extend Array to conform to CellViewDataSource. However, that's a pretty terrible idea. I suggest instead that you create a new type (probably a struct) that stores the data you need.

struct ModelA {
let name: String
let points: Int
let description: String
let image: UIImage
}

extension ModelA: CellViewDataSource {
func populate(cellView: UICellView) {
// Populate the cell view with my `name`, `points`, `description` and `image`.
}
}

class ElementData {
func validateModel() -> CellViewDataSource {
fatalError("Abstract method.")
}
}

class ClassA {
func validateModel() -> CellViewDataSource {
return ModelA(
name: "Bob Smith",
points: 123,
description: "A dummy model.",
image: someImage()
)
}
}

Use associated type protocol as a return type of a generic function

Protocols with associatedtype can’t be used in form of composition, which is a drawback and definitely irritating sometimes. But, you can create your own Type Erasure class to make this work.

You can study more about type erasure from this link: https://www.donnywals.com/understanding-type-erasure-in-swift/. You can find many more on Google.

This is how Apple has implemented it internally, by making few changes we can make it work our way.

Below is the code I came up with:

 //Let's say I have two possible type of responses
struct OtpResponse{}
struct SsoResponse{}

//A simple protocol to mandate the return of token from respective concrete type
protocol AuthenticationProvider{
associatedtype ResponseType
func getToken(completion: @escaping(ResponseType?, NSError?) -> Void)
}

//A type of auth provider
struct OtpBasedAuthProvider:AuthenticationProvider{

func getToken(completion: @escaping (OtpResponse?, NSError?) -> Void) {
let otpResponse = OtpResponse()
completion(otpResponse,nil)
}
}

//Another type of auth provider
struct SsoBasedAuthProvider:AuthenticationProvider{

func getToken(completion: @escaping (SsoResponse?, NSError?) -> Void) {
let ssoResponse = SsoResponse()
completion(ssoResponse,nil)
}
}

// there is some external logic to decide which type of auth provider to be used
func getProviderTypeFromSomeLogicOtherLogic() -> Int{
return 1//simply for dummy
}

Type Erasure:

class _AnyCacheBox<Storage>:AuthenticationProvider{
func getToken(completion: @escaping (Storage?, NSError?) -> Void) {
fatalError("Never to be called")
}

}

final class _CacheBox<C:AuthenticationProvider>: _AnyCacheBox<C.ResponseType>{
private var _base:C

init(base:C) {
self._base = base
}

override func getToken(completion: @escaping (C.ResponseType?, NSError?) -> Void) {
_base.getToken(completion: completion)
}
}

struct AnyCache<Storage>:AuthenticationProvider{
private let _box: _AnyCacheBox<Storage>

init<C:AuthenticationProvider>(cache:C) where C.ResponseType == Storage {
_box = _CacheBox(base: cache)
}

func getToken(completion: @escaping (Storage?, NSError?) -> Void) {
_box.getToken(completion: completion)
}
}


//Factory to return a concrete implementaton of auth provider
class AuthProviderFactory{
func getOTPAuthProvider() -> AnyCache<OtpResponse>{

let obj : AnyCache = AnyCache(cache: OtpBasedAuthProvider())
return obj

}

func getSSoAuthProvider() -> AnyCache<SsoResponse>{
let obj : AnyCache = AnyCache(cache: SsoBasedAuthProvider())
return obj
}
}

Below is how client can invoke methods in Factory-:

func executeNetworkCall() -> Void{
let factory = AuthProviderFactory()
let authProvider = factory.getOTPAuthProvider()
authProvider.getToken{(resp,error) in
//some code
print(resp)
}
}

It’s a bit involving and could take time to understand.

How to define a Protocol with generic function and some View as return type

This is explicitly disallowed by the Associated Type Inference section of the Opaque Result Types proposal. From the proposal:

Associated type inference can only infer an opaque result type for a non-generic requirement, because the opaque type is parameterized by the function's own generic arguments. For instance, in:

protocol P {
associatedtype A: P
func foo<T: P>(x: T) -> A
}

struct Foo: P {
func foo<T: P>(x: T) -> some P {
return x
}
}

there is no single underlying type to infer A to, because the return type of foo is allowed to change with T.

To make this more specific to your question, in order to conform to Test, there must be exactly one type that can be assigned to Result. However, your return type is generic, so it depends on the what is passed. The actual (non-opaque) return type of navigate is:

_ConditionalContent<NavigationLink<T, Text>, EmptyView>

But T is a type parameter and changes depending on how navigate is called. So there is no one type that can be assigned to Result.

You'll need something that can return a single, non-parameterized type. For the example you've given, that's probably AnyView, which is annoying.

That said, what you've written here doesn't really feel like a protocol. It looks a lot like just a function. I'd think a lot about how many different ways navigate could be written. If everyone would implement it the same way, that's just a function. (If you give another example of a conforming type, it might help to design a better approach.)

Swift generics & protocols: Return specialized type from static function?

You can use AssociatedTypes in your protocol definition.

protocol DAO {
associatedtype Item
static func fromDictionary(_ dictionary: [String : Any]) -> Item
}

class User : DAO {

static func fromDictionary(_ dictionary: [String : Any]) -> User {
return User(dictionary)
}
}

Read the official docs for AssociatedTypes



Related Topics



Leave a reply



Submit