Normally in the discussion of things like some
, they are referring to return types. This question is specifically about using any
or some
in argument lists.
As an example, in the Swift documentation for String
, you have this initializer...
init<T>(_ value: T, radix: Int = 10, uppercase: Bool = false)
where T : BinaryInteger
In Swift 5.6, they introduced the any
keyword to let us work with existential types more easily. With that change, I understand you theoretically can rewrite the above like so...
init(_ value: any BinaryInteger, radix: Int = 10, uppercase: Bool = false)
Of course there's also this version based on the some
keyword, which also works...
init(_ value: some BinaryInteger, radix: Int = 10, uppercase: Bool = false)
My question is... which one makes the most sense? Is there a down-side to using the existential type over the generic like that? What about the some
version? I was originally thinking yes, the generic version is best because the compiler can determine at compile time what's being passed to it, but then again, so does the existential any
and even the some
version as it won't compile if you don't pass it a BinaryInteger
and I'm not quite sure how to write a test to check this out.
When passing a value of a protocol type P
to a method in Swift, there are 4 possible spellings you can currently use:
func f<T: P>(_ value: T)
("generic")func f(_ value: some P)
("opaque parameter")func f(_ value: P)
("'bare' existential")func f(_ value: any P)
("'explicit' existential")Of these spellings, (1) and (2) are synonyms, and currently, (3) and (4) are synonyms. Between using (1) and (2), there is no difference, and between (3) and (4) there is currently* no difference; but between (1)/(2) and (3)/(4), there is a difference.
f<T: P>(_: T)
is the traditional way of taking a parameter of a concrete type T
which is guaranteed to conform to protocol P
. This way of taking a parameter:
T
both at compile time and at runtime, so you can perform operations on T
itselfT
is known at compile time, and the compiler knows the size and layout of the value and can set up the stack/registers appropriately; it can pass whatever parameter was given to it directly to the methodSelf
- or associatedtype
requirementssome Protocol
is exactly equivalent to the generic version spelled with angle brackets. I'll avoid repeating the content of the proposal, but the Introduction section spells out the desire for this syntax as a way to simplify the complexity of spelling generic parametersf(_: P)
is the traditional way of taking a parameter of an existential type which is guaranteed to conform to protocol P
. This way of taking a parameter:
type(of:)
P
and can dynamically pass methods along to the underlying concrete value. This both has a cost of allocating an additional "box" at runtime to hold the actual value inside of a consistently-sized and -laid-out container, as well as the cost of indirecting as method calls on the box must dynamically dispatch to the underlying typeSelf
- or associatedtype
requirementsany
keyword preceding a protocol type is a marker that helps indicate that the type is being used as an existential. It is exactly equivalent right now to using the bare name of the protocol (i.e. any P == P
), but there has been some discussion about eventually using the bare name of the protocol to mean some P
insteadSo to address your specific example:
init(_ value: some BinaryInteger, radix: Int = 10, uppercase: Bool = false)
is exactly equivalent to the original
init<T>(_ value: T, radix: Int = 10, uppercase: Bool = false)
where T : BinaryInteger
while
init(_ value: any BinaryInteger, radix: Int = 10, uppercase: Bool = false)
is not. And, because BinaryInteger
has associatedtype
requirements, you also cannot use the any
version as the existential type would not provide access to the underlying associated types. (If you try, you'll get the classic error: protocol 'BinaryInteger' can only be used as a generic constraint because it has Self or associated type requirements
)
In general, generics are preferable to existential types when possible because of the lack of overhead and greater flexibility; but, they require knowing the static type of their parameter, which is not always possible.
Existentials are more accepting of input, but are significantly more limited in functionality and come at a cost.
Between preferring <T: P>
and some P
, or P
and any P
— the choice is currently subjective, but:
any
keyword to denote existential protocol usage; the counterpart to using this is some
, so if you want to start future-proofing your code and staying consistent, it might be a good idea to start migrating to any
and some