goofy4224
goofy4224

Reputation: 310

Swift: Question on Generic Functions and Associated Types

Say, I have a protocol with an associated type:

protocol Model {
    associatedtype ModelType
}

And there's a generic struct conforming to this protocol:

struct Implementation<T>: Model {
    typealias ModelType = T
}

Now, I want to add an instance method to this struct accepting and returning a value of the abstract Model type:

struct Implementation<T>: Model {
    typealias ModelType = T
    func test<M: Model>(model: M) -> M {
        return model
    }
}

This compiles without errors. I want to go further and make sure that both the model parameter and the return value of the test(model:) method have the same associated ModelType as the Implementation instance this method is called on:

struct Implementation<T>: Model {
    typealias ModelType = T
    func test<M: Model>(model: M) -> M where M.ModelType == T {
        return model
    }
}

This compiles without errors, and it works as expected. If there's a second type conforming to Model

struct Alternative<T>: Model {
    typealias ModelType = T
}

…, I can pass an instance of this type to the test(model:) method only if it has the same associated ModelType. That is, the following code compiles…

let implementation = Implementation<Int>()
let alternativeA = Alternative<Int>()
_ = implementation.test(model: alternativeA)

…, whereas the following code:

let implementation = Implementation<Int>()
let alternativeB = Alternative<String>()
_ = implementation.test(model: alternativeB)

… fails to compile with an error saying,

Instance method 'test(model:)' requires the types 'Int' and 'Alternative<String>.ModelType' (aka 'String') be equivalent

Now, under some condition, the test(model:) method should return self. If I change it to (always) return self, however…

struct Implementation<T>: Model {
    typealias ModelType = T
    func test<M: Model>(model: M) -> M where M.ModelType == T {
        return self
    }
}

…, I'll get an error saying,

Cannot convert return expression of type 'Implementation<T>' to return type 'M'

This is confusing, as, clearly, the type of self should match all given constraints. (Or doesn't it?)

The error is, in fact, unrelated to the where clause. If I drop it…

struct Implementation<T>: Model {
    typealias ModelType = T
    func test<M: Model>(model: M) -> M {
        return self
    }
}

…, the error will persist:

Cannot convert return expression of type 'Implementation<T>' to return type 'M'

Why am I getting this error, and how can I return self from this method?

(I'm using Swift 5.3.2 by the way in case this matters.)

Upvotes: 0

Views: 446

Answers (2)

maximkrouk
maximkrouk

Reputation: 259

Generic functions always try to infer type from the context and at compile time

// func test<M: Model>(_ model: M) -> M
test(Alternative<Int>())

is treated as concrete type function

func test(_ model: Alternative<Int>) -> Alternative<Int>

so u cant do

struct Implementation<T>: Model {
    typealias ModelType = T
    func test(_ model: Alternative<Int>) -> Alternative<Int> {
        return self // Implementation<T>, type mismatch
    }
}

Also if you using a generic constraint for your generic function within a type

struct Implementation<T>: Model {
    typealias ModelType = T
    func test<M: Model>(_ model: M) -> M where M.ModelType == T {
        return model
    }
}

For Implementation<Int> translated into something like

extension Implementation where T == Int {
    func test<M: Model>(_ model: M) -> M where M.ModelType == Int {
        return model
    }
}

The constraint prevents you from calling

Implementation<Int>().test(Alternative<String>())

Maybe this proposal will let you fix the issue, but I think it will still be impossible, because this case cannot be resolved in compile time. Proposal

I'm not sure if you really want to return self or model from your method, maybe it's better to pass your model as an inout parameter. If you still want to return self you may do something like this

struct Implementation<T>: Model {
    typealias ModelType = T
    func test<M: Model>(_ model: M) -> Self where M.ModelType == T {
        return self
    }
}

You may overload this method to let compiler decide (from the context) what to use

struct Implementation<T>: Model {
    typealias ModelType = T
    func test<M: Model>(_ model: M) -> Self where M.ModelType == T {
        return self
    }

    func test<M: Model>(_ model: M) -> T where M.ModelType == T {
        return model
    }
}

But it may cause some type inference issues if M == Self

Btw you already creating an existential for your protocol by making a protocol witness

Protocol wintesses

Also I'm not sure if you really need to create a few witness types for your Model protocol, maybe you can generalize your work just using Implementation<T> extensions with generic constraints

Upvotes: 1

theMomax
theMomax

Reputation: 1069

You are getting this error, because indeed self does not match all given constraints. By stating func test<M: Model>(model: M) -> M where M.ModelType == T you only say that this type M (which is only exists in the context of a call to your test function) is to be a Model, which's ModelType is the same as the ModelType of self. That does not mean, however, that Self and M are equivalent.

A little example using your types from above:

Assuming self is an instance of Implementation<Int>. In the context of a call to test(model:), M could be either of Implementation<Int> or Alternative<Int>. Returning self wouldn't be a problem in the first case, as both instances have the same type. However, in the second case you could assign an Implementation<Int> to an Alternative<Int>.

var a = Alternative<Int>()
let x = Implementation<Int>()
// This would assign something of type `Implementation<Int>` to a variable that
// may only contain `Alternative<Int>`.
var a = x.test(model: a)

I am sure there is a method to do what you want to achieve in Swift, but the solution totally depends on what that is.

Upvotes: 1

Related Questions