Swiftwater Logo

Swift Generics

This entry is part 11 of 21 in the series Swiftwater

ABSTRACT

Generics are a fairly common structure in modern programming languages (Here’s an interesting paper that discusses them in several languages -not Swift. It is a bit old. Here’s basically the same paper, with a slightly modified format.). Swift implements a fairly basic set of them (compared, say to C++ Templates). C++ Templates were where I first encountered the concept of generic programming. I became quite enamored of the idea, but it was a real pain to debug generic code (C++ liked to put all the template stuff in the header, and debuggers didn’t handle headers -I understand it’s different, these days).

One of the things that I like about Swift’s implementation of generics, is the way that they are completely integrated into the language; often “disappearing” if there is no need to explicitly mention them. I’ll show how that works shortly. Basically generics are not an extension of Swift; added after the initial language. They are an integral part of Swift, and many of Swift’s fundamental data types rely on generics.

WHY GENERICS?

Generics are extremely useful in reusable or extensible library code. You generally won’t explicitly use generics if you’re writing “one-off” code, as they add a bit of extra complexity (but you are quite likely to use them implicitly, in Swift). Their true power lies in allowing you to write code that can be repurposed after you have written it, in ways that you can’t/won’t anticipate at the time that you write it.

For example, say that you are writing a collection class. It’s an aggregator of some kind (like an Array or a Dictionary). Your initial use may store Strings, but you’d like to be able to use the same class for numbers and objects.

The “old-fashioned” way of doing things would have you duplicate the original Strings aggregator, then modify the new copy to handle the new data type.

With generics, this is not necessary. You can write a single class that can be used with multiple data types right out of the box. It helps to enforce the fundamental programming principle of DRY (Don’t Repeat Yourself).

It should be noted that Swift generics are a pretty “light duty” implementation of generics, compared to languages like C++, but it’s not meant to be a heavy-duty algorithm language. Swift errs on the side of ease of implementation, type safety and security.

Generics are quite useful if you are implementing Protocol-Oriented Programming. They make classes reusable, as opposed to inheritable. The thing about reuse, is that it can easily violate “DRY,” if you do “copy and paste” programming, so it needs some kind of language-based construct to support it (the traditional way to do this has been to refactor the code to have a “non-denominational” base class, and specific subclasses, aimed at each implementation).

The way that Swift implements both protocols and generics makes it almost ideal for Protocol-Oriented Programming. You don’t need a base class or subclasses.

THE COST

Well-written generics can be fairly simple, but, in some languages, they require the implementor to know about the generic syntax, and apply that when using generics. Swift is nice, in that it can actually hide the generic syntax unless it’s required (It’s called “Type Inference“). It reduces the “cost” of having the implementor knowing about how generics work.

SYNTAX

Swift uses the same basic syntax as C++ Templates and Java Generics, where generics are specified between a less-than (<) and a greater-than (>) symbol, enclosing a type “placeholder.” Swift’s main difference from other languages is that you often don’t need this syntax.

The “Placeholder” is replaced by a type (not a value) at compile time, so this gives you the ability to reapply the same code to multiple uses.

Example: Swift Arrays

Remember what I said about generics being fundamentally integrated into the Swift language? One of the core data types for Swift is the Array, a simple random-access LIFO collection.

There are basically five ways to define an Array. Say we want to create an Array of String:

let aStringArray = ["Some String","AnotherString"]  // Direct type assignment
let myFirstArray = aStringArray     // Implicit type assignment, where "aStringArray" is already a String Array.
let mySecondArray:[String] = []     // Explicit basic type definition, using brackets. This is the most common form.
let myThirdArray:Array<String> = [] // This is the first (explicitly defined) generic form.
let myFourthArray = Array<String>() // This is the second (type-assigned) generic form.

The last two forms (red) are the ones that use generic syntax, and they prove that the Array type in Swift is actually a generic type. Generics really are an integral, fundamental part of the Swift programming language. Generics are how you can create Arrays of almost any data type.

Note that we seldom use the last two (generic) forms when we declare and use Arrays. Swift hides the generic from us.

let’s Go to the Literature

Swift is now a completely open-source language, so it’s possible to actually dig into it’s innards, and “read the entrails,” so to speak. Let’s look at the Array definition:

public struct ${Self}<Element>: _DestructorSafeContainer {
  %if Self == 'Array':
  #if _runtime(_ObjC)
    internal typealias _Buffer = _ArrayBuffer<Element>
  #else
    internal typealias _Buffer = _ContiguousArrayBuffer<Element>
  #endif
  %elif Self == 'ArraySlice':
    internal typealias _Buffer = _SliceBuffer<Element>
  %else:
    internal typealias _Buffer = _${Self.strip('_')}Buffer<Element>
  %end

  @_versioned
  internal var _buffer: _Buffer

  /// Initialization from an existing buffer does not have "array.init"
  /// semantics because the caller may retain an alias to buffer.
  @_inlineable
  @_versioned
  internal init(_buffer: _Buffer) {
    self._buffer = _buffer
  }

  %if Self == 'ArraySlice':
  /// Initialization from an existing buffer does not have "array.init"
  /// semantics because the caller may retain an alias to buffer.
  @_inlineable
  @_versioned
  internal init(_buffer buffer: _ContiguousArrayBuffer<Element>) {
    self.init(_buffer: _Buffer(_buffer: buffer, shiftedToStartIndex: 0))
  }
  %end
}

Man, that’s some serious gobbledygook, eh?

That “${Self}<Element>” is where the actual declaration happens. Most of the mechanics of what is going on there is beyond the scope of this posting (also, it seems to be pretty sparsely documented). I may go down that rabbit-hole in future posts. It’s interesting stuff, but only marginally useful in standard app programming. I simply wanted to show that there is actually a place to see the fundamental documentation for even the most basic Swift types.

Note that the syntax Apple uses for simple generics is “<Element>“. You don’t need to explicitly say “Element”. It can be any character or identifier string, but they like to use “Element”, where possible. C++ likes to use “<T>(You will also see Apple using this form extensively in their documentation. The person that created Swift was a C++ programmer).

The Apple Generic Language Reference

It’s always a good idea to know you can get the definitive information from the people that wrote the language. That’s the official Apple language reference on Swift generics.

Generic Parameter Clauses

Let’s look at a few ways to define generics:

typealias SwiftGenericType<T> = T

struct SwiftGenericStruct<T> {
    let value: T
    init(value: T) { self.value = value }
}

class SwiftGenericClass<T> {
    let value: T
    init(value: T) { self.value = value }
}

func swiftGenericEqualityTester<T: Comparable>(_ leftSide: T, _ rightSide: T) -> Bool { return leftSide == rightSide }

There’s also a couple of things that WON’T work:

// You can't do this. It's an error.
// typealias SwiftGenericTuple = Tuple<T>( key: T, value: T )
// You can't do this. It's an error.
// protocol SwiftGenericProtocol<T> {}

Constraints

Note the function has a modifier in its generic declaration:

func swiftGenericEqualityTester<T: Comparable>(_ leftSide: T, _ rightSide: T) -> Bool { return leftSide == rightSide }

This tells the compiler that you can stick any type in there that you like, but whatever type it is, needs to conform to the Comparable Protocol. This is called a Constraint.

In the next post, we’ll cover the use of constraints in associated type protocols, and give some more concrete examples.

Let’s try the function:

let is1EqualTo1 = swiftGenericEqualityTester<Int>(1, 1)

Damn. That didn’t work. It looks like the compiler doesn’t want us to specify an Int in the generic slot (remember what I said about Swift “hiding” generic syntax? This is an example). Let’s try taking that out:

let is1EqualTo1 = swiftGenericEqualityTester(1, 1)

That worked. Swift wants to let the compiler determine the types from the actual parameters passed in. In this case, the two parameters were both Int, and the compiler made T into Int.

Now, lets; try something else:

let is2EqualTo2Point5 = swiftGenericEqualityTester(2, 2.5)

That resulted in false. However, the two types were different. Parameter 1 was Int, and Parameter 2 was Double.

…or was it that way?

As it turns out, no. The compiler did not magically accept an Int and a Double. The compiler determined that T was Double, and cast both parameters to Double.

Test it out:

let leftSide: Int = 2
let rightSide: Double = 2.5
let is2EqualTo2Point5 = swiftGenericEqualityTester(leftSide, rightSide)

That won’t work. You’ll get an error.

There is a way that you can have two different types. You can specify multiple generic placeholders:

func swiftGenericEqualityTester2<T: Comparable, S: Comparable>(_ leftSide: T, _ rightSide: S) -> Bool { return true }
let rightSide1: Int = 2
let leftSide1: Double = 2.6
let is2EqualTo2Point6 = swiftGenericEqualityTester2(leftSide1, rightSide1)

However, we cheated. Note that we don’t compare. If we tried, we’d get an error. Swift is smart enough to know that being comparable is not enough:

func swiftGenericEqualityTester3<T: Comparable, S: Comparable>(_ rightSide: T, _ leftSide: S) -> Bool { return leftSide == rightSide }

Maybe we need to extend the constraint with a where clause:

func swiftGenericEqualityTester3<T: Comparable, S: Comparable>(_ rightSide: T, _ leftSide: S) -> Bool where T == S { return leftSide == rightSide }

Damn. That didn’t work. Swift doesn’t like the fact that the two types would be the same.

It’s entirely possible that there’s a way to do it, but we probably shouldn’t. My experience is that when you need to hack, your design needs a good review.

WE’RE JUST GETTING STARTED

I’m trying not to make these introductory posts too long, so I’ll conclude the discussion of generics in Part Deux.


SAMPLE PLAYGROUND

let aStringArray = ["Some String","AnotherString"]  // Direct type assignment
let myFirstArray = aStringArray     // Implicit type assignment, where "aStringArray" is already a String Array.
let myDirectArray = [String]()      // Explicit empty Array definition, using brackets and an initializer. This is a common way to instantiate an Array.
let mySecondArray:[String] = []     // Explicit basic type definition, using brackets. This is the most common form.
let myThirdArray:Array<String> = [] // This is the first (explicitly defined) generic form.
let myFourthArray = Array<String>() // This is the second (type-assigned) generic form.

typealias SwiftGenericType<T> = T

struct SwiftGenericStruct<T> {
    let value: T
    init(value: T) { self.value = value }
}

class SwiftGenericClass<T> {
    let value: T
    init(value: T) { self.value = value }
}

func swiftGenericEqualityTester<T: Comparable>(_ leftSide: T, _ rightSide: T) -> Bool { return leftSide == rightSide }

let is1EqualTo1 = swiftGenericEqualityTester(1, 1)  // Casts both as Int
let is2EqualTo2Point5 = swiftGenericEqualityTester(2, 2.5)  // Casts both as Double

// You can't do this. It's an error.
// let is1EqualTo1 = swiftGenericEqualityTester<Int>(1, 1)

// You can't do this. It's an error.
// let leftSide: Int = 2
// let rightSide: Double = 2.5
// let is2EqualTo2Point5 = swiftGenericEqualityTester(rightSide, leftSide)

// You can do this, however.
func swiftGenericEqualityTester2<T: Comparable, S: Comparable>(_ leftSide1: T, _ rightSide: S) -> Bool { return true }
let leftSide1: Int = 2
let rightSide1: Double = 2.6
let is2EqualTo2Point6 = swiftGenericEqualityTester2(leftSide1, rightSide1)

// You can't do this. It's an error.
// func swiftGenericEqualityTester3<T: Comparable, S: Comparable>(_ leftSide: T, _ rightSide: S) -> Bool { return rightSide == leftSide }

// You can't do this either. Swift doesn't want the types to be the same.
// func swiftGenericEqualityTester3<T: Comparable, S: Comparable> (_ leftSide: T, _ rightSide: S) -> Bool  where S == T { return rightSide == leftSide }

// You can't do this. It's an error.
// typealias SwiftGenericTuple = Tuple<T>( key: T, value: T )

// You can't do this. It's an error.
// protocol SwiftGenericProtocol<T> {}