The article is from collated notes on objccn. IO/related books. “Thank you objC.io and its writers for selflessly sharing their knowledge with the world.”

Generic programming is a code reuse technique that preserves type safety. For example, the library uses generic programming to make the sort method accept a custom comparison operator that takes the same type of argument as the elements in the sorted sequence. Similarly, Array is a generic type to provide an API for accessing and modifying arrays in a type-safe manner.

This usually refers to programming after type generalization (a notable syntactic feature is to use Angle brackets to visualize these generalized types, e.g. Array

). However, the concept of generics goes far beyond generalizing types. We can think of generics as a form of polymoyphism, which is a phenomenon in which an interface or name can be accessed through multiple types.

There are at least four different concepts that fall under the category of polymorphic programming:

  • You can define multiple methods with the same name but of different types. For example, three different sort are defined, each with a different parameter type. This usage is called overloading or, more technically, an AD hod polymorphism (specifically set up to solve the problem of sorting).

  • When a function or method takes class C as an argument, we can also pass it a derived class of C. This usage is called subtype polymorphism.

  • When a function takes a generic parameter (via Angle bracket syntax), we call that function a generic function, and similarly, generic types and generic methods. This usage is called parametric polymorphism. These generic parameters are called generics.

  • You can define a protocol and have multiple types implement it. This is another, more structured, proprietary polymorphism.

The third technique, parameterized polymorphism, is discussed.

The generic type

One of the most common functions you write is identify function. For example, a function that returns an argument as is:

func identity<A> (_ value: A) -> A { 
    return value
}
Copy the code

This identity function has A generic type: for any type A, the function is of type (A) -> A. However, this function has an infinite number of concrete types, that is, types that do not take generic parameters.

Functions and methods are not the only generic types. You can also have generic structures, generic classes, and generic enumerations. For example, here is the definition of Optional:

enum Optional<Wrapped> { 
    case none
    case some(Wrapped)}Copy the code

Optional is a generic type. Selecting a value for Wrapped yields a concrete type, such as Optional

or Optional

. Think of Optional as a type constructor: Pass it a concrete type (e.g., Int) and it creates a new concrete type (e.g., Optional

).


If you browse the Swift standard library, you’ll see that it contains many concrete types, but also many generic types (for example, Array, Dictionary, and Result). Array has a generic Element parameter, which allows us to create arrays using any of the concrete types. You can also create your own generic types. For example, here is an enumeration describing binary trees:

enum BinaryTree<Element> {
case leaf
indirect case node(Element, l: BinaryTree<Element>, r: BinaryTree<Element>)}Copy the code

BinaryTree is a generic type with a single generic parameter. To create a concrete type, we must set a concrete type for Element, such as Int:

let tree: BinaryTree<Int> = .node(5, l: .leaf, r: .leaf)
Copy the code

When converting a generic type to a concrete type, only one generic parameter corresponds to one concrete type. For example, when creating an empty array, we must provide an explicit array type, otherwise the Swift compiler will complain that it cannot confirm the exact type of the array elements:

var emptyArray: [String] = []
Copy the code

Similarly, Swift does not allow different types of values to be stored in arrays unless it is explicitly specified that elements in an Array can be represented by different types of values.

let multipleTypes: [Any] = [1."foo".true]
Copy the code

Extend generic types

In the scope of BinaryTree, the generic Element parameter is available. For example, when writing an extension to BinaryTree, you can use Element as if it were a concrete type. You can add a convenience initialization method for BinaryTree that takes an Element as an argument:

extension BinaryTree { 
    init(_ value: Element) {
        self = .node(value, l: .leaf, r: .leaf)
    }
}
Copy the code

Next, there is a calculated property that stores all the node values in the tree as an array and returns them:

extension BinaryTree { 
    var values: [Element] {
        switch self { case .leaf:
            return []
        case let .node(el, left, right):
            return left.values + [el] + right.values 
        }
    } 
}

// When we call values in BinaryTree
      
       , we get an array of integers:
      

tree.values / / [5]
Copy the code

You can also define generic methods. For example, add a map method to BinaryTree. This method takes an additional generic parameter, T, that represents the return value of the conversion method, which is the type of the value of the node in the new BinaryTree. Since this method is also defined in the BinaryTree extension, you can still use Element:

extension BinaryTree {
    func map<T> (_ transform: (Element) - >T) -> BinaryTree<T> {
        switch self { 
        case .leaf:
            return .leaf
        case let .node(el, left, right):
            return .node(transform(el), 
                         l: left.map(transform),
                         r: right.map(transform))
        } 
    }
}
Copy the code

Since neither Element nor T has protocol constraints, you can use any type here. It is even ok to make the two generic parameters the same concrete type:

let incremented: BinaryTree<Int> = tree.map { $0 + 1 }
// node(6, l: BinaryTree<Swift.Int>.leaf, r: BinaryTree<Swift.Int>.leaf)
Copy the code

It’s ok if it’s a different type. In the following example, Element is an Int and T is a String:

let stringValues: [String] = tree.map { "\ [$0)" }.values / / / "5"
Copy the code

In Swift, many collection types are generic (for example, Array, Set, and Dictionary). However, generics itself is more than just a technique for expressing collection types. Its application almost runs through the implementation of the Swift standard library, for example:

  • Optional abstracts the type it wraps with generic arguments.
  • Result takes two generic parameters: the types of values corresponding to success and failure results.
  • Unsafe[Mutable]PointerUse generic arguments to indicate the type of the object to which the pointer points.
  • Key Paths uses generics to represent the root type as well as the value type of the path.
  • A variety of types representing scopes, using generics to represent the upper and lower bounds of scopes.

Generics and Any)

In general, generics and Any have similar uses, but they have very different behavior. In programming languages without generics, Any is often used to achieve the same effect as generics, but without type safety. This usually means using some run-time feature, such as introspection or dynamic type casting, to turn an undefined type, such as Any, into a deterministic concrete type. Generics not only solve most of the same problems, but also bring the added benefits of compile-time type checking and improved runtime performance.

When reading code, generics can help you understand what a function or method does. For example, for the following reduce method that handles arrays:

extension Array {
    func reduce<Result> (_ initial: Result._ combine: (Result.Element) - >Result) -> Result
}
Copy the code

Without looking at its implementation, its signature gives a rough description of what it does:

  • First, Result is the generic parameter of Reduce and its return value type (where Result is just the name of the generic parameter).
  • Second, look at the parameters. Reduce takes a value of type Result and a method that combines Result and Element into a new Result value.
  • Since the return value is of type Result, the return value of reduce can only be initial or the Result of calling Combine.
  • If the array is empty, there is no Element for composition and only initial is returned.
  • If the array is not empty, the type of reduce gives its implementation freedom: it can simply return INITIAL and not merge the elements of the array at all, it can merge only specific elements of the array (for example, only the first or last element), or it can merge each element of the array individually.

Reduce can be implemented in an infinite number of ways. For example, call Combine only for certain elements in an array. It can also use some runtime type introspection features, modify some global state, or make some network requests. Because the library already implements the Reduce method, its implementation confirms what constitutes a reasonable implementation.

Now let’s look at the Any version of reduce:

extension Array {
    func reduce(_ initial: Any._ combine: (Any.Any) - >Any) -> Any
}
Copy the code

Even assuming a reasonable implementation of Reduce, this declaration conveys too little type information from the signature alone. Nothing can be learned from this signature about the relationship between the first parameter and the method return value, nor from Combine about how its two parameters are actually combined. In fact, you don’t even know that you combine the last cumulative result and the next element in the array.

In my experience, generic types are a great help in reading source code. Rather, whenever we see a function or method like Reduce or Map, we don’t have to guess what it does. The generic type in the signature alone constrains possible implementations.