What is the preferred way to declare a type parameter of a function?

Hello, community,
I want to write a function/method with a type parameter, specifically when the input type have constraints like: T: A, in which A is a class type or a non-PAT protocol type, I have 2 options.

func foo<T: A>(_ type: T.Type, ...)   // 1
func foo(_ type: A.Type, ...)  // 2

I can invoke either with foo(MyClass.self, ...)

Which is the preferred way in Swift?
Is the former one more efficient in some circumstances?

I’ve tried to generate intermediate SIL code and LLVM IR code based on this great post:

Source:

protocol P {}
func foo<T: P>(_ type: T.Type) {}

protocol P {}
func foo(_ type: P.Type) {}

SIL:

// foo<A>(_:)
sil hidden @$S3one3fooyyxmAA1PRzlF : $@convention(thin) <T where T : P> (@thick T.Type) -> () {
// %0                                             // user: %1
bb0(%0 : $@thick T.Type):
  debug_value %0 : $@thick T.Type, let, name "type", argno 1 // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function '$S3one3fooyyxmAA1PRzlF'

// foo(_:)
sil hidden @$S3one3fooyyAA1P_pXpF : $@convention(thin) (@thick P.Type) -> () {
// %0                                             // user: %1
bb0(%0 : $@thick P.Type):
  debug_value %0 : $@thick P.Type, let, name "type", argno 1 // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function '$S3one3fooyyAA1P_pXpF'

And the LLVM IR:

// foo<T>(_:)
define hidden swiftcc void @"$S3one3fooyyxmAA1PRzlF"(%swift.type*, %swift.type* %T, i8** %T.P) #0 {
entry:
  ret void
}

// foo(_:)
define hidden swiftcc void @"$S3one3fooyyAA1P_pXpF"(%swift.type*, i8**) #0 {
entry:
  ret void
}

The result is that this is all fine & nerdy, but I still don’t know the answer.

I think that the generic one is more performant but has bigger binary size (but both are probably not relevant in a common use case). At the semantic level they're very similar* if you only need to work with the given interface and not on memory-level details.

* should be the difference between "this T type conforming to P" and "a type conforming to P"

should be the difference between "this T type conforming to P" and
"a type conforming to P"

This matters sometimes. For example, in option 1 you can use T on the right side of an is operator.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Yeah the main difference is whether the code can use static knowledge about the type or has only access to runtime details. But with "you can use T on the right side of an is operator", don't you mean something like this?

protocol P {}
struct T: P {}

let t: P = T()
if t is T { print("T") }

Because is is a runtime check so it can be done with an existential as well

It seems the one using generics always wins: https://twitter.com/slava_pestov/status/1125843524203765761?s=21

It's not quite that simple. The optimizer pass for converting existential parameters to generic parameters helps if you call the function with an existential built out of a concrete type, because then we can specialize the generic parameter to the concrete type.

However, none of that should influence the way you write your code; the fact that the optimizer can convert one to the other gives you more freedom, not less.

3 Likes

However, none of that should influence the way you write your code

Thanks, it's very enlightening. After reading your post, I came to realize that the 2 ways of declaring foo have very different semantic meanings.

For example, because a protocol does not naturally conforms to itself, if I have an existential of protocol type P, whose value can only be determined at runtime, the only way to pass it to foo is declaring the function in the 2nd way: func foo(_ type: P.Type, ...).