Why is generic type bounds conformance on function types and their return types not stricter?

I've done some searching of the forum and couldn't tell if this question has been asked before.

Given the following code:

struct SomeResult<T: Codable>

func doSomething<T: Decodable>() -> SomeResult<T> { /* do stuff */ }

why does the compiler not flag this as an error given that not all things that are Decodable are necessarily Encodable?

This can lead to situations where the caller of doSomething cannot readily tell looking just at the function signature that they actually must use a type that conforms to Codable and not Decodable as the function claims. Also super confusing if you are writing a library that presents doSomething as an API method because when writing it, you can't tell that you'll be imposing this requirement on callers.

Is this intentional language design or a compiler bug? I come from Kotlin where the compiler would flag this as an error and prevent you from proceeding until you make sure the generic constraints "match up."

2 Likes

This is intentional. The constraint is part of the function's signature, and it is enforced; however it does not have to be written because it is inferred.

When we build the generic signature of a function or subscript declaration, we consider the requirements written in the where clause, but we also perform an inference pass by looking at the function's parameters and results.

The inference recursively walks each parameter and result type to look for generic types where the arguments are type parameters (the function's generic parameters or associated types thereof). If the generic type in question has its own requirements (like T : Codable in your example) we substitute the left hand side of the requirement with the corresponding generic argument (also T here, but it's the T belonging to doSomething()) and introduce this as a new requirement on the function.

Another example:

func foo<S : Sequence>(_: S, set: Set<S.Element>)

Since Set is declared as Set<Element> where Element : Hashable, the generic signature of foo() becomes

func foo<S : Sequence>(_: S, set: Set<S.Element>) where S.Element : Hashable
9 Likes

Got it, thanks. Does that mean the trade-off of possible confusion for callers was considered and not deemed a significant enough issue?

The feature predates the evolution process and even Swift 1.0 itself, so we can't really take it out now without massively breaking source. I'm honestly not a huge fan of it myself ;-) However there are three things to keep in mind:

  • Re-stating an inferred requirement is not considered a redundancy, so you won't get a warning about it like you will with duplicate explicit requirements
  • The generated interface view shown by SourceKit in Xcode includes inferred requirements
  • It's a pretty straightforward desugaring that doesn't really mess with language semantics in a fundamental way

In theory a linter tool with enough awareness of language semantics could flag definitions where one or more inferred requirements are not explicitly stated in the declaration.

4 Likes

Got it. Thank you, Slava, appreciate your insight and your time.