Abstract types and classes in Swift
2014-07-13
Abstract
Popular object-oriented programming languages support the concept of abstract classes as a means to define classes that are incomplete from an implementation perspective while providing a common supertype for all complete implementations. The methods for which an implementation is missing are typically called abstract methods. Subclassing is used to add implementations for abstract methods. Only classes without abstract methods can be instantiated.
Just like in Objective-C, the concept of an abstract class does not exist in Swift 2. This blog post explains how programmers can work around this limitation. It explains how abstract classes can be encoded and extended. The post will also show that there are interesting limitations in the type system of Swift 2 which lead to further complications when dealing with generic abstract classes, i.e. abstract classes with type parameters.
The classical approach
Let’s assume we would like to implement a framework for defining generic buffers. If Swift would have abstract classes, we could implement a generic Buffer class like this, assuming the modifier @abstract could be used for defining abstract classes and methods:
@abstract class Buffer<T> {
@abstract func append(value: T)
@abstract func generator() -> () -> T?
func process(p: (T) -> Void) {
...
}
func mapTo<S>(buffer: Buffer<S>, f: (T) -> S) {
...
}
func fold(var initial acc: T, with f: (T, T) -> T) -> T {
...
}
}
In this example, class Buffer<T> defines an abstract append and generator method as well as three concrete methods process, mapTo, and fold. The implementations of these methods may refer to the abstract methods append and generator. A subclass of Buffer<T> only needs to implement append and generator and will simply inherit the other concrete methods defined in the Buffer<T> superclass.
Separating interfaces from implementations
The idiomatic way to achieve something very similar in Swift 2 is to use protocols and protocol extensions. A protocol defines a new type in terms of an interface of a class, struct, or enum.
Here is the definition of the BufferType protocol declaring the interface of all buffer objects:
protocol BufferType {
typealias Value
func append(value: Value)
func generator() -> () -> Value?
func process(p: (Value) -> Void)
func mapTo<T, B: BufferType where B.Value == T>(buffer: B, f: (Value) -> T)
func fold(var initial acc: Value, with f: (Value, Value) -> Value) -> Value
}
Swift 2 supports extensions of protocols, which define implementations for some of the methods of the protocol. The following extension of BufferType provides implementations for the methods process, mapTo, and fold.
extension BufferType {
func process(p: (Value) -> Void) {
let generate = generator()
while let next = generate() {
p(next)
}
}
func mapTo<T, B: BufferType where B.Value == T>(buffer: B, f: (Value) -> T) {
process { buffer.append(f($0)) }
}
func fold(var initial acc: Value, with f: (Value, Value) -> Value) -> Value {
let generate = generator()
while let next = generate() {
acc = f(acc, next)
}
return acc
}
}
Note that the method implementations in this protocol extension are able to refer to methods like append and generator which are left unimplemented — these are the abstract methods in the first listing above. Implementations of protocol BufferType need to provide implementations for the missing methods only. All other methods of the protocol are contributed by the BufferType extension.
Below is a complete implementation of BufferType. It uses arrays as a means to store the elements of the buffer. In addition to implementing the abstract methods append and generator, ArrayBuffer also implements the CustomStringConvertible protocol by providing a computed property description of type String.
class ArrayBuffer<T>: BufferType, CustomStringConvertible {
var segments: [T] = []
func append(value: T) {
segments.append(value)
}
func generator() -> () -> T? {
var i = 0
return { i < self.segments.count ? self.segments[i++] : nil }
}
var description: String {
if segments.count == 0 {
return "<>"
}
var res = "<(segments[0])"
for i in 1..<segments.count {
res += ",(segments[i])"
}
return res + ">"
}
}
The following lines show how ArrayBuffer can be used:
let abuf = ArrayBuffer<Int>()
abuf.append(1)
abuf.append(2)
abuf.append(3)
print(abuf)
abuf.process(print)
print("sum = (abuf.fold(initial: 0) { $0 + $1 })")
This is what the script will print out when executed:
<1,2,3>
1
2
3
sum = 6
Limitations of protocols with associated types
Let’s assume we wanted to provide a more conventional map method in our buffer abstraction which simply takes a mapping function and returns a buffer over the mapped values. In a language with abstract classes, this method can be directly added to the abstract Buffer<T>class:
@abstract class Buffer<T> {
...
func map<S>(f: (T) -> S) -> Buffer<S> {
...
}
}
Here is a first attempt to introduce map into the BufferType protocol in a similar fashion in Swift 2:
protocol BufferType {
...
func map<T>(f: (Value) -> T) -> BufferType
}
Since Swift does not allow us to constrain the result type of this method such that it refers to only BufferType objects whose Value type is equivalent to T, the compiler has to reject this code. It will yield the error: “Protocol ‘BufferType’ can only be used as a generic constraint because it has Self or associated type requirements.”
Consequently, the next attempt to define a signature for map is to use BufferType as a constraint of a new type variable B. This allows us to declare that we require B.Value to be equivalent to T:
protocol BufferType {
...
func map<T, B: BufferType where B.Value == T>(f: (Value) -> T) -> B
}
While this does compile, it turns out to be very difficult to come up with an implementation matching this new map signature. This is because type B is an arbitrary type, implicitly inferred on the client side where map is called. Within the body of map itself, we do not know how to construct new buffers of type B.
Since Swift supports constructor polymorphism, we can fix this issue by requiring that all BufferType implementations support a specific constructor, e.g. one that creates a new empty buffer. This can be expressed in Swift in the following way:
protocol BufferType {
init()
...
func map<T, B: BufferType where B.Value == T>(f: (Value) -> T) -> B
}
An implementation in a protocol extension is straightforward now. The highlighted line in the next listing shows how a new buffer of type B is constructed.
extension BufferType {
func map<T, B: BufferType where B.Value == T>(f: (Value) -> T) -> B {
let res = B()
process { res.append(f($0)) }
return res
}
}
With this new map method, it is now possible to map buffers to new buffers without creating the new buffer objects explicitly and without explicitly passing them as a parameter, e.g. as for mapTo. The context where map is invoked infers the right buffer type and passes this information in form of type parameter B to method map, which can now create new objects of type B as needed.
With this change, we can now use the new map method in the following way:
let buf = ArrayBuffer<Int>()
buf.append(1)
buf.append(2)
buf.append(3)
let res: ArrayBuffer<Int> = buf.map{$0 + 1}
print(res)
This code snippet compiles and prints <2, 3, 4>. Unfortunately, using map like this only works for very simple cases. For slightly more complicated scenarios, it is not possible to infer a concrete type for B which could be passed to method map. Note that this is not a limitation of the inference algorithm in general, but rather a consequence of the fact that there are situations where there is no best matching type available. Here’s an example which extends the script above by concatenating a second map invocation:
let res2: ArrayBuffer<String> = buf.map{$0 + 1}.map{"'($0)'"}
print(res2)
This code results in the following compiler error: “Cannot invoke ‘map’ with an argument list of type ‘((_) -> _)'”. In this case, the compiler cannot infer a suitable buffer type for the first map invocation. Since Swift does not allow users to provide type parameters explicitly, all we can do is to split the sequence of map invocations:
let res2: ArrayBuffer<Int> = buf.map{$0 + 1}
let res3: ArrayBuffer<String> = res2.map{"'($0)'"}
print(res3)
This code snippet now compiles and prints <'2','3','4'>. Note that the type declarations in the let constructs are absolutely necessary for this example to work, as those types define what target buffers to construct in the two map invocations.
Generic wrappers to the rescue
The tedious workaround that was needed to allow for a relatively simple sequencing of two subsequent map operations, shows that this approach to develop generic methods similar to map, isn’t really that useful in practice. Since the Swift designers must have encountered this problem early as well, they decided to use yet another workaround via generic wrappers.
A generic wrapper is an object implementing a protocol like BufferType by forwarding the methods to a different implementation of the same protocol. This way, they are hiding the actual implementation from clients. Previously, we were hiding the actual implementation type by first trying to use a protocol type directly, and later indirectly by using a new type variable constraint by the protocol type. Generic wrappers have the advantage that they can be used as a generic type, avoiding the typing complications that come from the limitations of protocol types in the type system of Swift 2. The remaining part of this blog post explains how to implement generic wrappers and how to use them in our BufferType abstraction.
Implementing a generic wrapper
In general, we are facing the same dilemma when implementing a generic wrapper for BufferType, since we cannot use BufferType within the wrapper as a type for the delegatee. Code along the lines of the following snippet, will not compile:
struct Buffer<T>: BufferType {
let delegatee: BufferType
init(_ delegatee: BufferType) {
self.delegatee = delegates
}
func append(value: T) {
delegatee.append(value)
}
func generator() -> () -> T? {
return delegatee.generator()
}
}
The best workaround is to pass in all the abstract methods individually in form of closures. The following listing outlines this design pattern.
struct Buffer<T>: BufferType {
let _append: (T) -> Void
let _generator: () -> () -> T?
init(append: (T) -> Void, generator: () -> () -> T?) {
_append = append
_generator = generator
}
init<B: BufferType where B.Value == T>(_ base: B) {
self.init(append: base.append, generator: base.generator)
}
func append(value: T) {
_append(value)
}
func generator() -> () -> T? {
return _generator()
}
}
Struct Buffer<T> stores the closures implementing the two abstract methods append and generate individually in properties. Line 4–7 define a designated initializer which receives the two closures and stores them each in a property. In addition, there is a convenience initializer defined on lines 8–10 which receives an arbitrary BufferType object and which calls the designated initializer with the corresponding methods of this object. With this second initializer, we are able to hide the implementation details of the generic wrapper (i.e. the fact that we decomposed it into individual closures) and clients can simply pass an existing BufferType object to the convenience initializer.
Using generic wrappers
We can now use the type of the generic wrapper, Buffer<T>, as the return type of method mapin BufferType. This has two advantages: the actual implementation type of the returned buffer is hidden, and we end up with a simple method signature which every Swift programmer will understand:
protocol BufferType {
...
func map<T>(f: (Value) -> T) -> Buffer<T>
}
Below is an implementation of method map in a protocol extension of BufferType. It is using an ArrayBuffer object internally to construct the result. In line 6, the result is wrapped in a Buffer object and returned.
extension BufferType {
...
func map<T>(f: (Value) -> T) -> Buffer<T> {
let res = ArrayBuffer<T>()
process { res.append(f($0)) }
return Buffer(res)
}
}
The next code snippet shows that it is now possible to concatenate the two map invocations without running into issues with the type system.
let buf = ArrayBuffer<Int>()
buf.append(1)
buf.append(2)
buf.append(3)
let generate = buf.map{$0 + 1}.map{"'\($0)'"}.generator()
while let next = generate() {
print(next)
}
This is the output:
'2'
'3'
'4'
Summary
In Swift, protocols and protocol extensions play the same role like abstract classes in languages like Java and C#
Protocols with abstract associated types or self types cannot be used as stand-alone types; they can only act as type constraints.
Generic methods can be used to workaround this limitation, but Swift’s type inference mechanism quickly reaches its limits, ultimately making this workaround not very attractive.
Generic wrappers encapsulate concrete protocol implementations; using their type instead of the protocol type is commonly used to for declarations referring to protocols with abstract associated types.