Generics, Opaque Types, Boxed Types

Prenez
VStackable
Published in
5 min readSep 30, 2023

--

a few explanatory drawings

Generics

Generics themselves were a response to a call for flexibility in function creation, answering a desire to be pass in different Types to a function, rather than a single Type, and, within constraints, have the function be able to handle them, without having to reproduce the function several times. Under the hood, Generics is syntactic sugar around parametric polymorphism, a kind of overloading.

Sounds great. So what’s the problem?

Using Generics, a caller specifies type being sent through the function interface to the implentation (1). So, for example, you specify a generic parameter T: Shape (a Type constrained by the Shape protocol) and pass in a concrete Type. Say, a Triangle. Now, inside the implementation, the Type Triangle can be seen by the compiler, providing static checking (2) and at runtime (4), providing dynamic checking.

The problem is really twofold: first, the internal types returned by the function (3) expose the type to any caller, and especially at the Module level, the Module creator may wish to hide those details. Second, as with SwiftUI, the actual Type of the concrete type can become so long as to be unreadable. The Type could literally go on for pages.

(Hmmm I can see I could improve the drawing my showing a module boundary as well…)

Generics

Opaque Type

Opaque Types solve the problem by using the ‘some’ keyword to hide the specific Type from the caller, instead specifying a Protocol that constrains the Type. (3)

THIS IS NOT TYPE ERASURE. The Type still exists and is still visible to the implementation. In fact, in this case, it’s not the Caller who specifies the Type, it’s the implementation (1). This let’s the Caller use the Type, but only the part constrained by the protocol, without exposing all the other details about the Type.

The compiler still gets to do static checking (2) because the Type is still visible to the implementation — the Type is only hidden from the Caller. And, of course, therefore, Runtime can still perform Dynamic checking (4).

Important point: in the implementation you cannot return two different Type conditionally. The compiler is still returning one type, even though the details are hidden, just as a regular non-opaque function type does. So you can’t do this:

func myFunc() -> some Shape{ <--compiler static checks for Type here

// NO!! You return Circle OR Square but not both
if x {
return Circle
}
else {
return Square

}
}
Opaque Types

Boxed Protocol Type

‘Existential Type’

“There exists a type T such that T comforms to the Protocol’.

Ok, this is a little more involved. Apple just added the ‘Any’ keyword. Why? Because developers were really confused about how Protocols and Type Erasure work. In particular, in the case where you create a var and use a Protocol as a Type, as in this case, where Shape is Protocol, not a Type:

var s = Shape()

…it was hard for developers (including me) to understand that s is a Boxed variable in this case. This is an existential par excellence. All we know about the Type is that it conforms to the protocol Shape. We know nothing else about it, other than that it exists.

So, while this still works, it will soon be removed or deprecated from the language, in favor of

var s = any Shape()

…to make it more obvious that this Type is wrapped.

On to the drawing. In (1) We see that the Caller is (A) sending two different types into the function (B) through the interface and then ( C) magic boxing happens (kind of like when Bruce Wayne and Dick Grayson would slide down the pole in Batman 66 and emerge wrapped in Batman and Robin costumes — their identities had been Type Erased!). Finally, we see that D) now they are brought into the implementation as Boxed, Type erased … things.

BECAUSE THE FUNCTION ONLY SEES THEM AS BOXED, THE COMPILER DOES NOT KNOW THEIR TYPE! (3) And so they cannot be statically checked. They can only be unboxed and dynamically checked at runtime (4).

In this example, I show that the struct containing an Array of Type Erased items has been returned. (3) If you look at the example page in Apple, you can see that these are Shapes and the draw() method is being called on each item (via a closure passed in through map()). This means each item must be unboxed (handle by the Runtime, you don’t have to do anything) in order to find the right method in the Protocol Witness table.

The point of this, and the advantage it provides, is that you can pass in heterogenous items into an array and then have them all dealt with. NOTE you could always do this, you just had to handle the wrapping yourself — you had to create some kind of a wrapper struct that held your items. Now any will do this for you.

FWIW I remember a couple of years after Swift came out, Type Erasure was the Topic of the Moment and I looked at it and I thought, they can’t be just talking about something as simple as wrapping objects in other objects, can they? Yes, that’s exactly what they were talking about. I still have no idea why this was so intellectually captivating, or why such a simple, matter of fact idea was wrapped in such obscure language.

I hope this helps someone get over the hump on understanding this. I’m going to integrate these visual ideas with how this works with Protocol Witness Tables and the synthesized enums that Eidhof describes in Advanced Swift.

As always, reminding folks these aren’t really articles, these are just my personal notes. Use them as you will.

--

--

Prenez
VStackable

Writes iOS apps in Swift and stories in American English.