You’ve probably heard the term: type erasure. You’ve even used the standard library’s type erasure (AnySequence). But what exactly is type erasure, and how do we implement it? This article is about that.

In everyday development, there is always a desire to hide a class or implementation details from other modules, or it will feel like they are all over the project. Or you want to convert between two different classes. Type erasure is the process of removing a type standard from a class and making it more generic.

It’s natural to think of protocols or abstracting superclasses to do this. A protocol or superclass can be considered a way to implement type erasure. Here’s an example:

NSString in the standard library we can’t get an instance of NSString, all the NSString objects that we get are actually private subclasses of NSString in the standard library. These private types are completely hidden from the outside world, and you can use the NSString API to use these instances. All subclasses we use do not need to know what they are, and therefore do not need to consider their specific type information.

Something more advanced is needed when dealing with generics and association-typed protocols in Swift. Swift does not allow protocols to be used as classes. If you want to write a method that takes a sequence of type Int. It’s not right to write:

func f(seq: Sequence<Int>){... }// Compile error: Cannot specialize non-generic type 'Sequence'
Copy the code

In this case, we should consider using generics:

func f<S: Sequence>(seq: S) where S.Element= =Int{... }Copy the code

I’ll just write it like this. However, there are a few cases where it’s a little tricky to express a return value type or an attribute using code like this.

func g<S: Sequence>(a) -> S where S.Element= =Int{... }Copy the code

Writing it this way is not going to be the result we want. In this line of code, we want to return an instance of a class that satisfies the condition, but this line of code will allow the caller to select the specific type he wants, and then the g method will provide the appropriate value.

protocol Fork {
    associatedtype E
    func call(a) -> E
}

struct Dog: Fork {
    typealias E = String
    func call(a) -> String {
        return "🐶"}}struct Cat: Fork {
    typealias E = Int
    
    func call(a) -> Int {
        return 1}}func g<S: Fork>(a) -> S where S.E= =String {
    return Dog(a)as! S
}

// It can be seen here. Exactly what g returns is determined at the time of the call. This means that to use g properly you must use code like 'let dog: dog = g()'
let dog: Dog = g()
dog.call()

// error
let dog = g()
let cat: Cat = g()
Copy the code

Swift provides the AnySequence class to solve this problem. AnySequence wraps an arbitrary Sequence and hides its type information. Then replace this with AnySequence. With AnySequence we can write the f and g methods above like this.

func f(seq: AnySequence<Int>){... }func g(a) -> AnySequence<Int> {... }Copy the code

So the generics are gone, and all the specific type information is hidden. Using AnySequence adds a little more complexity and running costs, but the code is cleaner.

There are many such types in the Swift standard library, such as AnyCollection, AnyHashable, AnyIndex, etc. You can define your own generics or protocols in your code, or simply use these features to simplify your code.

Class-based erasure

We need to wrap some common functionality from multiple types without exposing type information. It’s natural to think of abstract superclasses. We can actually do type erasure by abstracting superclasses. The parent class exposes the API, and the subclass does the specific implementation according to the specific type information. Let’s see how we can implement something like AnySequence ourselves.

class MAnySequence<Element> :Sequence {
Copy the code

This class needs to implement the iterator type as the return type of the makeIterator. We have to do two type erasings to hide the underlying sequence type and the iterator type. This built-in iterator type complies with the IteratorProtocol and uses fatalError in the next() method to throw an exception. Swift itself does not support abstract classes, so this is sufficient:

    class Iterator: IteratorProtocol {
        func next(a) -> Element? {
            fatalError("Must override next()")}}Copy the code

ManySequence implements the makeIterator method similarly, using fatalError to throw exceptions. This error is used to prompt subclasses to implement this function:

    func makeIterator(a) -> Iterator {
        fatalError("Must override makeIterator()")}Copy the code

This is the public API required for class-based type erasure. Private implementations need to subclass this class. The public class is parameterized by the element’s type, but the private implementation is within the type:

private class MAnySequenceImpl<Seq: Sequence> :MAnySequence<Seq.Element> {
Copy the code

This class requires internal subclasses to implement the two methods mentioned above:

class IteratorImpl: Iterator {
Copy the code

This step wraps the iterator type of the sequence

    class IteratorImpl: Iterator {
        var wrapped: Seq.Iterator
        
        init(_ wrapped: Seq.Iterator) {
            self.wrapped = wrapped
        }
    }
Copy the code

This step implements the next method. Is actually the next method that calls the iterator of the sequence it wraps.

        override func next(a) -> Element? {
            return wrapped.next()
        }
Copy the code

Similarly, MAnySequenceImpl is a wrapper for sequence.

    var seq: Seq
    
    init(_ seq: Seq) {
        self.seq = seq
    }
Copy the code

This step implements the makeIterator method. Get the iterator object from the wrapped sequence and wrap the iterator object into the IteratorImpl.

    override func makeIterator(a) -> IteratorImpl {
        return IteratorImpl(seq.makeIterator())
    }
Copy the code

One more thing is needed: initialize a MAnySequenceImpl with a MAnySequence, but the return value is marked as a MAnySequence type.

extension MAnySequence {
    static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where Seq.Element= =Element {
        return MAnySequenceImpl<Seq>(seq)
    }
}
Copy the code

Let’s use this MAnySequence:

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
        print(elt)
    }
}

let array = [1.2.3.4.5]
printInts(MAnySequence.make(array))
printInts(MAnySequence.make(array[1 ..< 4]))
Copy the code

Function-based erasure

** We want to expose functionality of multiple types without exposing those types. ** The natural approach is to store functions whose signatures refer only to the types we want to expose. The body of a function can be created in a context where the underlying information is known.

How to implement the MAnySequence? More like that. Only this time since we don’t need inheritance and it’s just a container, we’ll implement it with a Struct.

Again, declare a Struct

struct MAnySequence<Element> :Sequence {
Copy the code

As above, implementing the Sequence protocol requires an Iterator to return the value. This thing is also a struct and it has a store property, and the store property is a store property that takes no arguments, returns an Element, okay? The function. It is required by the IteratorProtocol

    struct Iterator: IteratorProtocol {
        let _next: () -> Element?
        
        func next(a) -> Element? {
            return _next()
        }
    }
Copy the code

MAnySequence is similar to this. It contains the store property of a function that returns Iterator. The Sequence is implemented by calling this function.

    let _makeIterator: () -> Iterator
    
    func makeIterator(a) -> Iterator {
        return _makeIterator()
    }
Copy the code

The init method of the MAnySequence is the most important. It accepts an arbitrary Sequence as an argument (Sequence

, Sequence

):

init<Seq: Sequence> (_ seq: Seq) where Seq.Element= =Element {
Copy the code

Then you need to wrap the required functionality of the Sequence in this function:

        _makeIterator = {
Copy the code

And then we need to make an iterator here that the Sequence happens to have:

var iterator = seq.makeIterator()
Copy the code

Finally we wrap the iterator around the MAnySequence. Its _next function calls iterator’s next function:

            return Iterator(_next: { iterator.next() })
        }
    }
}
Copy the code

See how this MAnySequence is used:

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
        print(elt)
    }
}

let array = [1.2.3.4.5]
printInts(MAnySequence(array))
printInts(MAnySequence(array[1 ..< 4]))
Copy the code

Done!

This function-based erasure approach is very effective when you need to wrap a small piece of functionality as part of a larger type, eliminating the need for a separate class to erase type information from other classes.

For example, we need to write some code that can be used on specific collection types:

class GenericDataSource<Element> {
    let count: () - >Int
    
    let getElement: (Int) - >Element
    
    init<C: Collection> (_ c: C) where C.Element= =Element.C.Index= =Int {
        count = { Int(c.count) }
        getElement = { c[$0 - c.startIndex]}
    }
}
Copy the code

Thus, the other code in GenericDataSource can directly manipulate the incoming collection using the count() and getElement() methods. And this collection type does not pollute the GenericDataSource’s generic parameters.

conclusion

Type erasure is a very useful technique. It is used to prevent generics from intruding into code and to make interfaces simpler. Separate the API from the specific functionality by wrapping the underlying type information. Type erasure can be done by using static public types or wrapping apis into functions. Function-based type erasure is especially useful in simple cases where only a few functions are required.

The Swift standard library provides some type erasure that can be used directly. AnySequence is a wrapper around a Sequence that, as the name suggests, allows you to iterate over a Sequence without knowing the exact type. AnyIterator is his good friend, providing an iterator whose type has been erased. AnyHashable wraps the type with the Hashable type erased. Swift also has several protocols based on collection types. Search for “Any” in the document. Type erasure is also available in Codable: KeyedEncodingContainer and KeyedDecodingContainer are both wrappers for protocol type erasure. They are used to implement encode and decode without knowing the specific type of information.

The last

Saw MikeAsh’s latest Friday Q&A Type Erasure in Swift the other day. I want to take advantage of nothing to translate recently. As a result, I have been addicted to eating chicken recently and have no time to do this. So…