I would like to create a function that returns an object that conforms to a protocol, but the protocol uses a typealias
. Given the following toy example:
protocol HasAwesomeness {
typealias ReturnType
func hasAwesomeness() -> ReturnType
}
extension String: HasAwesomeness {
func hasAwesomeness() -> String {
return "Sure Does!"
}
}
extension Int: HasAwesomeness {
func hasAwesomeness() -> Bool {
return false
}
}
String
and Int
have been extended to conform to HasAwesomeness
, and each implements the hasAwesomeness()
method to return a different type.
Now I would like to create a class that returns an object that conforms to the HasAwesomeness
protocol. I don't care what the class is, just that I can send the message hasAwesomenss()
. When I try the following, I generate a compile error:
class AmazingClass: NSObject {
func returnsSomethingWithAwesomeness(key: String) -> HasAwesomeness {
...
}
}
ERROR: Protocol 'HasAwesomeness' can only be used as a generic constraint because it has Self or associated type requirements
As you can imagine, the intention for returnsSomethingWithAwesomeness
is to return a String
or Int
based ok the key
parameter. The error the compiler throws kinda-sorta makes sense why it's disallowed, but it does give an insight into fixing the syntax.
func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T
{
...
}
Alright, my reading is the method returnsSomethingWithAwesomeness
is a generic method that returns any type T
that has the subtype HasAwesomness
. But, the following implementation throws more compile-time type errors:
func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T
{
if key == "foo" {
return "Amazing Foo"
}
else {
return 42
}
}
ERROR: Type 'T' does not conform to protocol 'StringLiteralConvertible'
ERROR: Type 'T' does not conform to protocol 'IntegerLiteralConvertible'
Alright, so now I'm stuck. Will someone please help fill in the gaps in my understanding about types and generics, possibly pointing me to useful resources?
I think the key to understanding what is going on here is distinguishing between things that are determined dynamically at runtime, and things that are determined statically at compile time. It doesn't help that, in most languages like Java, protocols (or interfaces) are all about getting polymorphic behavior at run time, whereas in Swift, protocols with associated types are also used to get polymorphic behavior at compile time.
Whenever you see a generic placeholder, like
T
in your example, what type is filled in for thisT
is determined at compile time. So, in your example:func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T
is saying:
returnsSomethingWithAwesomeness
is a function that can operate on any typeT
, just so long asT
conforms toHasAwesomeness
.But what is filled in for
T
is determined at the pointreturnsSomethingWithAwesomeness
is called – Swift will look at all the information at the call site and decide what typeT
is, and replace allT
placeholders with that type.*So suppose at the call site the choice is that
T
is aString
, you can think ofreturnsSomethingWithAwesomeness
as being rewritten with all occurrences of the placeholderT
replaced withString
:Note,
T
is replaced withString
and not with a type ofHasAwesomeness
.HasAwesomeness
is only being used as a constraint – that is, restricting what possible typesT
can be.When you look at it like this, you can see that that
return 42
in theelse
makes no sense – how could you return 42 from a function that returns a string?To make sure
returnsSomethingWithAwesomeness
can work with whateverT
ends up being, Swift restricts you to only use those functions that are guaranteed to be available from the given constraints. In this case, all we know aboutT
is that it conforms toHasAwesomeness
. This means you can call thereturnsSomethingWithAwesomeness
method on anyT
, or use it with another function that constrains a type toHasAwesomeness
, or assign one variable of typeT
to another one (all types support assignment), and that is it.You can’t compare it to other Ts (no guarantee it supports
==
). You can't construct new ones (who knows ifT
will have an appropriate initialize method?). And you can’t create it from a string or integer literal (doing that would requireT
to conform to eitherStringLiteralConvertible
orIntegerLiteralConvertible
, which it doesn’t necessarily – hence those two errors when you try and create the type using one of these kinds of literals).It is possible to write generic functions that return a generic type that all conforms to a protocol. But what would be returned would be a specific type, not the protocol so which type would not be determined dynamically. For example:
Think of
returnCollectionContainingOne
in this code as really being two functions, one implemented forContiguousArray
, and one forArray
, written automatically by the compiler at the point you call them (and therefore where it can fixC
to be a specific type). Not one function that returns a protocol, but two functions that return two different types. So in the same wayreturnsSomethingWithAwesomeness
can’t return either aString
or anInt
at runtime based on some dynamic argument, you couldn’t write a version ofreturnCollectionContainingOne
that returned either an array or a contiguous array. All it can return is aT
, and at compile time whateverT
actually is can be filled in by the compiler.* this is a slight oversimplification of what the compiler actually does but it’ll do for this explanation.