Types

Types

This page draws heavily from these swift.org references:

When you dive deeper into the intricate details of protocols, you encounter many types of type. Here’s a brief run-down, starting with some basic types of types.

  • Concrete type: Normal, everyday type with methods that are fully implemented, so you can create instances of it. It includes generic types.

  • Abstract class: A base implementation that other types can inherit from but whose methods aren’t implemented. It’s not formally supported in Swift and is often simulated by having methods throw fatalError if called by mistake. See below for an example of how protocols can support a more robust simulation.

  • Nominal type: Any type that has been explicitly named by a declaration somewhere in code. These can conform to protocols, be extended, and have values created using the initializer syntax MyType(). They’re not exactly the same as named type (see below).

  • Non-nominal type: Obtained by composing other types: function types like (Int) -> (String), tuple types like (Int, String), special types like Any and AnyObject, and metatypes like Int.Type. (A metatype type refers to the type of any type, including class types, structure types, enumeration types, and protocol types.) A protocol composition type lets you specify a value whose type conforms to the requirements of multiple protocols without explicitly defining a new, named protocol that inherits from each protocol you want the type to conform to. As you saw with DiskWritable & Encodable in the first lesson, this can be preferable to the tight coupling of inheritance.

Protocol: Nominal or Non-Nominal?

If a type is either nominal or non-nominal, where do protocols fit? You certainly name a protocol, but can you create an instance using MyProtocol()? Well, according to Nominal Types, protocols are non-nominal:

Because a protocol is named by a declaration in code, it may conform to (in other words, refine) other protocols and may be extended. However, when written as the type of a constant or variable such as let value: MyProtocol, the name refers to a distinct, non-nominal existential type that provides a “box” for a value of any concrete type that conforms to the protocol. The existential type itself does not conform to any protocols and cannot be extended, and a value cannot be created using the initializer syntax MyProtocol().

Here’s a simple example to confirm this:

protocol MyProtocol {
  var number: Int { get set }
  init(number: Int)
}

let mp = MyProtocol(number: 42)

Sure enough, trying to instantiate MyProtocol produces the error:

Type 'any MyProtocol' cannot be instantiated

On the other hand, protocols are a named type, according to Types, which specifies a slightly different dichotomy:

In Swift, there are two kinds of types: named types and compound types. A named type is a type that can be given a particular name when it’s defined. Named types include classes, structures, enumerations, and protocols.

So protocols are named but not nominal. They’re in a gray area, or maybe a better color is amber: Warning, this doesn’t behave like a normal type!

Boxed Protocol (Existential) & Opaque Types

Boxed protocol and opaque types are two ways to hide details about a value’s concrete type. A boxed protocol type is also called an existential type, which comes from the phrase “there exists a type T such that T conforms to the protocol”.

Boxed protocol type is the preferred, formal term for existential type — a type that conforms to a protocol or protocol composition, with the ability for that conforming type to vary while the program runs. It has the form you saw in the type-erasure section — any <#constraint#>. The constraint can be a protocol type, protocol-composition type, a metatype of a protocol type, or a metatype of a protocol-composition type.

An opaque type has the form some <#constraint#>. The constraint is a class type, protocol type, protocol-composition type, or Any. Opaque types appear as the return type of a function or subscript, or the type of a property. Opaque types can’t appear as part of a tuple type or a generic type, such as the element type of an array or the wrapped type of an optional.

At runtime, an instance of a boxed protocol type can contain a value of any type that satisfies the constraint. The additional level of indirection that’s used when working with a boxed protocol type is called boxing. Boxing typically requires a separate memory allocation for storage and an additional level of indirection for access, which incurs a performance cost at runtime.

At compile time, in contrast to boxed protocol types, a value with opaque type has a specific concrete type, and Swift can use that underlying type for optimizations. However, the opaque type forms a boundary that information about that underlying type can’t cross.

The following example is excerpted from Opaque and Boxed Protocol Types, which also contains an explanation of The Problem That Opaque Types Solve, a boxed-protocol example and a discussion of generics vs. opaque type vs. boxed-protocol type.

An opaque type is like the reverse of a generic type. Generic types let the code that calls a function pick the type for that function’s parameters and return value in a way that’s abstracted away from the function implementation.

func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }

The code that calls max(_:_:) chooses the values for x and y, and the type of those values determines the concrete type of T. The calling code can use any type that conforms to Comparable.

An opaque type lets the function implementation pick the type for the value it returns in a way that’s abstracted away from the code that calls the function.

func makeTrapezoid() -> some Shape { ... }

This function declares its return type as some Shape; as a result, the function returns a value of some given type that conforms to the Shape protocol, without specifying any particular concrete type. Writing makeTrapezoid() this way lets it express the fundamental aspect of its public interface — the value it returns is a shape — without making the specific types that the shape is made from a part of its public interface.

Self & Any Types

  • Self type: Not a specific type, it lets you conveniently refer to the current type without repeating or knowing that type’s name. In a protocol declaration or a protocol member declaration, the Self type refers to the eventual type that conforms to the protocol.

  • Any type: Can contain values from all other types. When you use Any as a concrete type for an instance, you need to cast the instance to a known type before you can access its properties or methods.

Simulating Abstract Classes

This example is from John Sundell’s Abstract types and methods in Swift. It discusses how to simulate an abstract class, with trade-offs when using a protocol with associated type vs. composing a class and a protocol.

The simplest way to simulate an abstract class is to implement its methods to throw fatalError:

class Loadable<Model> {
  func load(from url: URL) async throws -> Model {
    fatalError("load(from:) has not been implemented")
  }
}

If, instead, you use a protocol, the compiler won’t let you instantiate it, so you don’t need to throw fatalError:

protocol Loadable {
  associatedtype Model
  func load(from url: URL) async throws -> Model
}

class UserLoader: Loadable {
  func load(from url: URL) async throws -> User {
    ...
  }
}

You make Loadable generic, for flexibility, but this raises the need for type erasure when you want to use it as a concrete type. Another problem is that protocols can’t contain any form of storage. To add any stored properties that all Loadable implementations can use, you’d have to declare those properties in every one of those concrete implementations. This problem goes away if Loadable is a class.

So try this solution: Use a protocol only for the method, to ensure Loadable users implement it:

protocol LoadableProtocol {
  associatedtype Model
  func load(from url: URL) async throws -> Model
}

class LoadableBase<Model> {
  let networking: Networking
  let cache: Cache<URL, Model>

  init(networking: Networking, cache: Cache<URL, Model>) {
    self.networking = networking
    self.cache = cache
  }
}

This does mean that your concrete subclasses will have to conform to LoadableProtocol:

class UserLoader: LoadableBase<User>, LoadableProtocol {
  ...
}

Again, typealias is your friend:

typealias Loadable<Model> = LoadableBase<Model> & LoadableProtocol

You still can’t create concrete types from Loadable, so you still need to keep type erasure in your toolkit. Another potential problem is that type aliases like this can’t be extended, should you want to provide a few convenience APIs that you don’t want to (or can’t) implement directly in the LoadableBase class.

Sundell’s solution: Declare everything you need to implement the convenience APIs within LoadableProtocol, so you then can extend LoadableProtocol by itself:

protocol LoadableProtocol {
  associatedtype Model
  
  var networking: Networking { get }
  var cache: Cache<URL, Model> { get }
  
  func load(from url: URL) async throws -> Model
}

extension LoadableProtocol {
  func loadWithCaching(from url: URL) async throws -> Model {
    if let cachedModel = cache.value(forKey: url) {
      return cachedModel
    }
    
    let model = try await load(from: url)
    cache.insert(model, forKey: url)
    return model
  }
}

Try this out the next time you need an abstract class!

See forum comments
Download course materials from Github
Previous: Type Erasure Next: Protocols - Conclusion