Understanding Swift’s Abstract Types (Swift 2.1 and below)

Swift[1] helps us build more reliable software by providing generic entities that can be reused with different types while still supporting static type checking. In a construct such as struct Pair<T> { let x, y: T }, the “T” is an abstract type or type variable, because it stands for whatever type will eventually be used in the generic entity. In this particular case, “T” is a type parameter because it stands for a type that will be passed in to Pair when it is used. Swift also has other kinds of type variables such as associated types and type members.[2] These type variables can also occur in different contexts, and the rules for which kind of type variables can be used in which context can be confusing. Let’s see if we can simplify the rules by grouping the concepts together in a reasonable way.

Unconstrained Type Variables

A type variable may be constrained or unconstrained. We’ll start with the simpler, unconstrained case.

(Concrete) Aggregates: Obeying the same rules

An aggregate is an entity that collects together a bunch of things, such as constants, variables, and methods. Swift offers five constructs that could potentially be considered as aggregates:

  1. Classes,
  2. Enumerations,
  3. Functions,
  4. Protocols, and
  5. Structures

From the perspective of Swift’s type system, one of these things does not belong in this list. Which is it? Although functions seem different, it turns out to be protocols that don’t fit with the rest. Classes, enumerations, and structures may all be instantiated, and a function can be called; they are all concrete. But a protocol may only be used to describe something else. This difference is reflected in the type system, so henceforth, we will use the term concrete aggregate to refer to any of classes, enumerations, functions, or structures, but not protocols.

Type Members

A type member is a shorthand for another type that is defined by a typealias statement in a concrete aggregate[3] (but not in a protocol because the rules and meanings are different, as described under associated types): 

enum Grain { case Wheat, Corn }
struct Pizza {
    typealias CrustType = Grain  // defines a type member
    let crust: CrustType
}

In the example above, CrustType is a type member of the Pizza aggregate. It is referenced inside the aggregate to specify the type of the crust attribute. A type member can also be referenced outside of its enclosing attribute:

let nakedPizzaCrust: Pizza.CrustType

A type member cannot be defined to be a generic protocol as illustrated in the last of three attempts to define a type member to: an instantiated generic class, a non-generic protocol, and a generic protocol. The third attempt fails:

protocol CrustProtocol    {}
 struct   WheatCrust       {}
 class    Pizza<CrustType> {}
 protocol PizzaProtocol {
     // This associated type makes PizzaProtocol generic
     typealias CheeseType
 }
 struct Meal {
     // OK, Pizza is a (generic) class
     typealias TypeMember1 = Pizza<WheatCrust>  // Succeeds
     
     // OK, CrustProtocol is not generic
     typealias TypeMember2 = CrustProtocol      // Succeeds
       
     // Illegal: PizzaProtocol is a generic protocol
     typealias TypeMember3 = PizzaProtocol  // Fails!
 }

A type member can also be defined in an extension of a structure, class, or enumeration:

extension Meal {
     typealias TypeMember4 = Int
 }

Type Parameters

Concrete aggregates such as classes, enumerations, functions, and structures, (but not protocols) may be parameterized over different types by adding type parameters in angle-brackets. In the following examples, the type parameters are printed in boldface:

class  Box<Contents> { let contents: Contents;  }
struct Pair<Element> { let x, y: Element }
enum   Maybe<Result> { case Failure(String); case Success(Result) }
func   makePizza<Crust> ( crust: Crust ) -> Pizza {}

Although both type members and type parameters are associated with a concrete aggregate, a type member can be referenced from the outside (as shown above), but a type parameter cannot:

struct Pizza<GrainParameter> {
    typealias GrainMember = GrainParameter
    let crust1: GrainParameter
    let crust2: GrainMember
}
typealias CornPizzaType = Pizza<Grain>.GrainMember    // Works!
typealias CornPizzaType = Pizza<Grain>.GrainParameter // Fails!

Associated Types, in several flavors

When turning a structure into a generic, you introduce a type parameter as mentioned above. But the analogous operation on a protocol uses a different construction, called an associated type. It looks like the typealias in the section above, but means something(s) completely different, with correspondingly different usage rules.

Bound Associated Types: Here is the simplest kind of associated type, looking just like the type members discussed above:

protocol Edible {
     typealias G = Grain
     var carbohydrates: G { get }
 }

Unlike a type member, an associated type cannot be referenced outside of its enclosing scope, which can be confusing because the definition of a type member in a concrete aggregate looks exactly like this kind of associated type definition in a protocol:

struct   S { typealias CrustDepth = Int }
protocol P { typealias CrustDepth = Int }
let a: S.CrustDepth  // OK
let b: P.CrustDepth  // Illegal!

yet one can be used from the outside and the other cannot.

Because the typealias in Edible above binds G to a particular type, we’ll call it a bound associated type to distinguish it from the other kinds of associated types.

Name- and Occurrence-Matching Associated Types: An associated type need not be bound to another type when it is introduced.[4]. Here are examples of the two kinds of unbound associated types:

protocol PizzaProtocol {
     typealias CrustDepth  // not used within this protocol
     typealias CrustGrain  // used for grain and flour
     var grain: CrustGrain {get}
     var flour: CrustGrain {get}
 }

Neither CrustDepth nor CrustGrain is followed by an equals sign (‘=‘) where they are introduced; that’s why I call them unbound. They are merely placeholders that must be bound when a concrete aggregate adopts the protocol. But their meanings differ:  CrustDepth is not used within PizzaProtocol so all that is required is for an adopting concrete aggregate to possess a type member with the same name:

struct ConformingPizza: PizzaProtocol {
     typealias CrustDepth = Int // what’s needed for CrustDepth
         // see below for the rest
 }

 Let’s call this construction a name-matched associated type.

CrustGrain is used within the protocol, so an adopting concrete aggregate is required to posses both grain and flour attributes of the same type; the name of that type does not matter:

struct ConformingPizza: PizzaProtocol {
     typealias CrustDepth = Int
     let grain: Wheat // No mention of CrustGrain
     let flour: Wheat
     
 }

Let’s call this construction an occurrence-matched associated type.

Had the types of grain and flour differed, the structure would not have conformed to the protocol:

struct NonconformingPizza: PizzaProtocol { // Illegal!
     typealias CrustDepth = Int
     let grain: Wheat // No mention of CrustGrain
     let flour: Corn
     
 }

In summary, an associated type is defined with a typealias statement inside of a protocol. There are three different varieties:

  1. A bound associated type is defined with an equals sign, and declares a shorthand for a type-expression.
  2. A name-matched associated type is defined without an equals sign and is not referenced within the protocol (or anywhere else). A conforming concrete aggregate must include a type member of the same name. A matching type parameter won’t do. This is the only variety for which the name of the type is visible to an adopter.
  3. An occurrence-matched associated type is defined without an equals sign and is referenced within the protocol. A conforming concrete aggregate must include attributes whose names match those in the protocol. Every occurrence of the associated type in the protocol must correspond to an occurance of a unique type in the aggregate. The name of the unique type does not matter.

Like a type parameter, an associated type cannot be used outside of the protocol.

A protocol extension may introduce only bound associated types.

extension PizzaProtocol {
     typealias Diameter = Int // OK
     typealias Cheese         // Illegal!
 }

Summary: Unconstrained Type Variables

A type variable is an identifier that can be bound to different types. There are three basic kinds:

  1. A type member is defined inside of a structure, function, class, or enumeration with a typealias statement and an equals sign (‘=‘). A type member can be used outside of its defining context in the same fashion as any other attribute. It functions as a shorthand for the type expression on the right hand side of the equals sign. A type member may be defined to be any type except for a generic protocol.
  2. A type parameter is defined inside of angle brackets after the name of a structure, function, class, or enumeration. A type parameter can only be used within its defining context.
  3. An associated type is defined inside of a protocol with a typealias statement. Although it resembles a type member, it cannot be used outside of its defining protocol. There are three kinds:
    • A bound associated type is defined with an equals sign (‘=‘) and functions as a shorthand for the type expression, just as the type member.
    • A name-matched associated type has nothing after its name in the typealias statement, and has no uses in the protocol. In order to conform to the protocol a concrete aggregate must define a type member of exactly the same name.
    • An occurrence-matched associated type also has nothing after its name in the typealias statement, but does have uses in the protocol. Its name is irrelevant to conformance testing; but each of its occurrences in the protocol must correspond to an occurrence in an attribute of the conforming concrete aggregate.

The table below gives examples:

kind of type variable

In

purpose

example

visible outside

type member

classes, enums, structures and extensions

define shorthand

struct S {
  typealias NumT = Int
}
let x: S.NumT

yes

type member

functions

define shorthand

func f() {
  typealias NumT = Int
}

no

type parameter

classes, enums, structures, functions

generalize concrete aggregate to different static types

class Pizza<CrustGrain> {…}
func eat<FoodT>(food: FoodT)  {…}

no

associated type (bound)

protocol, protocol extension

define shorthand

protocol P {
  typealias NumT = Int
}
struct S: P {}

no

associated type (name-matched)

protocol

enforce that implementors define a shorthand of this name

protocol P {
  typealias NumT // no other mention
}
struct S: P {
  typealias NumT = Int
}

no

associated type (occurrence-matched)

protocol

enforce a type pattern among attributes of an implementor

protocol P {
  typealias NumT
  var x: NumT {get}
  var y: NumT {get}
}
struct S: P { let x, y: Int }

no

Constrained Type Variables

When a definition applies for only a subset of possible types, the relevant type variable may be constrained. For example, the type variable CrustType for the crust instance variable of the generic Pizza class is constrained to be some kind of (more precisely, the same as or a subtype of) PizzaCrust:

protocol PizzaCrust {
     var hasGluten: Bool {get} // “:” means “has concrete type”
 }
 struct WheatCrust: PizzaCrust { // “:” means “conforms to protocol”
     let hasGluten = true
 }
// The “:” below means
//   “is constrained to”, in other words,
//   “will be assigned a type that is the same or a subtype of”.
class Pizza<CrustType: PizzaCrust> {
// The “:”s below mean “will have the same type that is assigned to”
    let crust: CrustType
    init(_ c: CrustType)  { self.crust = c }
}
let aNYPizza = Pizza(WheatCrust())
aNYPizza.crust.hasGluten // returns true

The type constraint ensures the type safety of the hasGluten query to crust  because the runtime type of crust, a class, enumeration, or structure, must conform to the PizzaCrust protocol and thus must implement hasGluten with a Bool result.

In the example above the colon (“:”) occurs multiple times, with distinct, though analogous meanings:

  • hasGluten: Bool, denoting that a variable (hasGluten) has a specific type (Bool),
  • CrustType: PizzaCrust, denoting that a type variable (CrustType) can only be bound to the same- or any sub-type of a type variable (PizzaCrust, which in this case is a type parameter),
  • let crust: CrustType, denoting that a variable (crust) will have whatever specific type a type variable (CrustType) is bound to, and
  • init(_ c: CrustType), denoting that a parameter (c) must be the same as or any subtype of whatever specific type a type variable (CrustType) is bound to.

Let’s take a closer look at the second usage, in which the colon constrains a type variable to be a subtype (or the same type as) some type expression.

Only Classes and Protocols are subtypeable

In Swift, only protocol- and class-types may have subtypes; others may not. I’ll use the term subtypeable for these types. (With respect to subtypeability, it does not matter whether an entity is generic or not.) When a colon constrains a type variable to a type expression, the type expression must represent a subtypeable type. For example, T: Int is illegal because Int is a structure, but T: UIViewController is OK because UIViewController is a class.  

In the example above, CrustType was constrained to be a subtype of PizzaCrust, a protocol and hence, subtypeable. Had PizzaCrust been a structure instead of a protocol, the line:

class Pizza<CrustType: PizzaCrust> {}

would have been illegal because structures are not subtypeable.

Type Constraint rules vary according to the kind of type variable

Swift includes many different contexts in which a type may be constrained. Recall the three kinds of type variables: type members, type parameters, and associated types.

Type Members: A type member cannot be constrained.

Type Parameters: You have already seen a constraint on the CrustType parameter where it was introduced. A constraint may also be placed on a type parameter when its containing generic class, enumeration, or structure is extended:

protocol Crust             {…}
 protocol WheatCrust: Crust {…}
 protocol  CornCrust: Crust {…}

struct Pizza<CrustType: Crust> { // Constrained at introduction
    let crust: CrustType
}

// Below, CrustType is constrained when Pizza is extended:
extension Pizza where CrustType: WheatCrust {…}

Associated Types: An associated type behaves somewhat like a type parameter, only for a protocol instead of a concrete aggregate. Just as a type parameter may be constrained by the subtyping (colon) operator, so can an associated type:

protocol PizzaCrust {…}
 protocol Food       {…}

class Pizza<CrustType: PizzaCrust>: Food { // constrained type parm.
    let crust: CrustType
    init(crust: CrustType) { self.crust = crust }
}
protocol PizzaProtocol {
    typealias CrustType: PizzaCrust // constrained assoc. type
    var crust: CrustType {get}
}

In addition to subtyping constraints, an associated type may be constrained to an exact type with the equality operator (‘==‘), as in the where clause below:

func eatWheatPizza <
    WheatPizza: PizzaProtocol  where WheatPizza.CrustType == WheatCrust
>
(aPizza: WheatPizza) {…}

The type equality operator can only be used on an associated type. Unlike the colon, which enforces a subtype constraint, the equality operator enforces an identity constraint: the type variable on the left must be exactly the same type as a type expression on the right.[5]

In exactly the same fashion as with a type parameter, an associated type may be constrained with a “where” clause when its protocol is extended.

Summary: Constrained Type Variables

Where a type parameter or associated type is introduced (i.e. inside of angle brackets, or with a typealias), it may be constrained (with a colon) to be a subtype of a class or protocol, possibly in a where clause.  

When extending an entity, its type parameters or associated types may be similarly constrained in a where clause. 

When extending a protocol, an associated type may be constrained in a where clause to exactly match a given type by using the equality operator (“==“).

kind of type variable

context

kind of constraint

constrained to

type parameter or
associated type

introduction or
extension

subtype

class or
protocol

associated type

extension

identity

any type

As the table above shows, the oddball case involves the identity constraint.

Conclusions

A concrete aggregate is a Swift class, enumeration, function, or structure. In order to support generic concrete aggregates, Swift includes abstract types, also called type variables. To master Swift, one must master the rules its type variables, but these rules have many dissimilar cases, and have not heretofore been collected in one place. That is the purpose of this writeup. 

A type variable can be either: a type parameter, defined in angle brackets; a type member, defined in a concrete aggregate by a typealias statement; a bound associated type, defined by a typealias statement with an equals sign (‘=‘) in a protocol, a name-matched associated type, defined by a typealias statement in a protocol but not referenced in that protocol; or an occurrence-matched associated type, defined by a typealias statement in a protocol and referenced in that protocol. Each of these five kinds of type variable has different usage requirements and meanings.

A type variable may optionally be constrained. A type parameter or associated type may be constrained with a colon (‘:’) to indicate that it must be the same or a subtype of a type expression, which in turn must resolve to either a class or protocol type.

When extending a protocol, an associated type may be constrained to be identical to the value of a type expression with the equality symbol (‘==‘).

Acknowledgements

Kristen McIntyre contributed materially to the quality of this work with several rounds of careful proofreading and suggestions for improvement. The Swift developer community provided much inspiration with their excellent articles and presentations on using Swift. The Swift team at Apple has performed an amazing feat of language design, creation and implementation. IBM Research funded the work.

Glossary

Because this essay has a more narrow focus than the Swift book[6], I have found it helpful to use some slightly differing terminology. Presented here is a map from the terminology in this essay to related terms in the Swift book:

terms in this essay

related terms in the Swift book

concrete aggregate

any of: class, enumeration, function, structure

associated type

associated type, protocol-associated-type-declaration, typealias

bound associated type

typealias-assignment

subtype constraint

subtype constraint, type-inheritance

subtypeable

either of: class or protocol

type member

typealias, typealias-declaration, typealias-assignment

type parameter

type parameter, generic parameter

type variable

abstract type


[1] Swift is a trademark of Apple Inc., registered in the U.S. and other countries. This document is based on Swift 2.1.

[2] In pursuit of clarity, I use some terms that differ from those in the Swift book. See the glossary at the end.

[3] Alexis Gallagher has a nice presentation online at https://cur.at/7sUTN9E?m=email&sid=4BPCwl2.

[4] They may be constrained, as explained in the section on constrained types.

[5] Unlike most meanings of equality, Swift’s type equality constraint operator is not commutative.

[6] The Swift ProgrammingLanguage (Swift 2.1), Swift Programming Series, Apple Inc. 2015