Using Protocols to Build a (very) Generic Deque
(Download the playground to use the code and see the outputs)
This post is an update on a previous implementation of a Deque. A full implementation of this Deque is available here.
A Deque is a data structure comprised of two stacks, facing opposite directions. In this way, operations at either end of the Deque have the same complexity as operations on one end of the underlying stack. This implementation uses two arrays, with the front reversed: appending, prepending, and removal of the first and last elements are all (amortized) O(1).
The standard library has three Array
structs:
Array
,
ArraySlice
, and ContiguousArray
. They all have the same
interface, with different underlying implementations. An Array
is a
standard vector-like structure, which allows O(1) amortized appending,
fast iteration, etc. A ContiguousArray
has stricter rules about
contiguity, but it’s not bridged to Objective-C.
= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let array : ContiguousArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] let cArray
An ArraySlice
is a reference
into an Array
or ContiguousArray
, for more efficient
slicing. All the information an ArraySlice
contains is the beginning and
end points of the slice (as well as any changes made to the slice
separate from the array)
= array[0..<6] let slice
To replicate these semantics in a Deque requires three separate
structs: one with an Array
as the
stack, another with an ArraySlice
as the stack, and a third with a ContiguousArray
. The standard library
seems to duplicate the structs, along with their methods and
properties.
It would be much nicer to just define a protocol that represented the difference between the deque types: then you could just write the methods and properties once, on top of it. Something like this:
{
protocol DequeType Container : RangeReplaceableCollectionType, MutableSliceable
typealias var front: Container { get set }
var back : Container { get set }
init()
}
There’s one problem with this: both stacks need to be made public. It would be much nicer to hide the stacks (especially since an invariant needs to be checked and maintained on every mutation). If anyone has an idea of how to accomplish that, tweet me.
The first method to implement is a subscript. Indexing is difficult, because the front stack will be reversed, so the index used to get in to the Deque will need to be translated into an equivalent index in the array.
Any (valid) index will point into either the front or back queue, and
the transformations applied to it in each case is different. If it’s in
the front, the end result will look like front[front.endIndex - 1 - i]
,
whereas if it’s in the back, it should be back[i - front.endIndex]
.
There’s nothing specified about the Containers except that they’re RangeReplaceableCollectionType
and MutableSliceable
, so the index types
will have to be as generic as possible. (you could specify where Index == Int
,
but that’s more specific than needed, and not very extensible.)
Both of those transformations are subtractions, an operation that’s
possible on RandomAccessIndexType
s with the advancedBy
method. advancedBy
takes the associated Distance
type of the RandomAccessIndexType
. That’s enough
information to figure out that the Deque’s index type must be the same
as the Distance of the Index of the Container.
{
extension DequeType = Container.Index.Distance
typealias Index }
The method that will translate an index into the relevant index in the stacks will return an enum:
<I> {
public enum IndexLocationcase Front(I), Back(I)
}
Then, the translate method itself:
extension DequeType whereContainer.Index : RandomAccessIndexType,
Container.Index.Distance : ForwardIndexType {
private func translate(i: Container.Index.Distance)
-> IndexLocation<Container.Index> {
return i < front.count ?
.Front(front.endIndex.predecessor().advancedBy(-i)) :
.Back(back.startIndex.advancedBy(i - front.count))
}
}
This performs two steps: 1. Check which stack it’s in. 2. Subtract in the appropriate order
: Deque = [1, 2, 3, 4, 5, 6] // [1, 2, 3 | 4, 5, 6]
let d
.translate(0) // Front: 2
d.translate(4) // Back: 1 d
This means that the logic for converting distance to index is separated from the logic for actual indexing. Great! Here’s the indexing:
extension DequeType whereContainer.Index : RandomAccessIndexType,
Container.Index.Distance : ForwardIndexType {
var startIndex: Container.Index.Distance { return 0 }
var endIndex : Container.Index.Distance { return front.count + back.count }
subscript(i: Container.Index.Distance) -> Container.Generator.Element {
{
get translate(i) {
switch case let .Front(i): return front[i]
case let .Back(i): return back[i]
}
} set {
translate(i) {
switch case let .Front(i): front[i] = newValue
case let .Back(i): back[i] = newValue
}
}
}
}
This makes things much easier to test and debug.
Here’s where the power of protocols becomes obvious. If you go back
to the original definition of DequeType
, you can add Indexable
. It may seem like now only
indexable things can conform, but what happens in practice is that when
Indexable
looks for its
requirements, it can use the implementations in DequeType. That
means that we’ve just made anything that can conform to DequeType
indexable. That’s awesome.
Next job is ranged indices. This is a good bit more complicated than the individual indices, so it definitely will benefit from being separated into a translate method:
extension DequeType whereContainer.Index : RandomAccessIndexType,
Container.Index.Distance : BidirectionalIndexType {
private func translate
(i: Range<Container.Index.Distance>)
-> IndexRangeLocation<Container.Index> {
if i.endIndex <= front.count {
= front.endIndex.advancedBy(-i.endIndex)
let s if s == front.startIndex && i.isEmpty { return .Between }
= front.endIndex.advancedBy(-i.startIndex)
let e return .Front(s..<e)
}
if i.startIndex >= front.count {
= back.startIndex.advancedBy(i.startIndex - front.count)
let s = back.startIndex.advancedBy(i.endIndex - front.count)
let e return .Back(s..<e)
}
= front.startIndex..<front.endIndex.advancedBy(-i.startIndex)
let f = back.startIndex..<back.startIndex.advancedBy(i.endIndex - front.count)
let b return .Over(f, b)
}
}
: Deque = [0, 1, 2, 3, 4, 5] // [0, 1, 2 | 3, 4, 5]
let otherDeque
.translate(0...2) // Front: 0..<3
otherDeque.translate(4...5) // Back: 1..<3
otherDeque.translate(2...5) // Over: 0..<1, 0..<3
otherDeque.translate(3..<3) // Between otherDeque
The invariant that must be maintained in the deque is this: if either stack has more than one element, the other cannot be empty. If the invariant is violated, the longer stack is reversed, and put in place of the shorter.
{
public enum Balance case FrontEmpty, BackEmpty, Balanced
}
{
extension DequeType
var balance: Balance {
public let (f, b) = (front.count, back.count)
if f == 0 {
if b > 1 {
return .FrontEmpty
}
} else if b == 0 {
if f > 1 {
return .BackEmpty
}
}
return .Balanced
}
var isBalanced: Bool {
public return balance == .Balanced
}
}
A deque is a good data structure for certain uses, especially those
that require popping and appending from either end. popFirst()
and popLast()
aren’t included in the standard RangeReplaceableCollectionType
, though,
so we’ll have to add our own.
: BidirectionalIndexType {
extension RangeReplaceableCollectionType where Index private mutating func popLast() -> Generator.Element? {
return isEmpty ? nil : removeLast()
}
}
var mutableDeque: Deque = [0, 1, 2, 3, 4, 5]
.popLast() // 5
mutableDeque// [0, 1, 2 | 3, 4]
mutableDeque
Container.Index : BidirectionalIndexType {
extension DequeType where popLast() -> Container.Generator.Element? {
public mutating func return back.popLast()
}
}
The method needs to include check()
,
which we can do with defer
popLast() -> Container.Generator.Element? {
mutating func { check() }
defer return back.popLast()
}
.popLast() // 4
mutableDeque// [0, 1, 2 | 3]
mutableDeque .popLast() // 3
mutableDeque// [0 | 1, 2] mutableDeque
You also can’t just pop from the back queue in popLast()
,
because it may be the case that the front stack has one element left
popLast() -> Container.Generator.Element? {
mutating func { check() }
defer return back.popLast() ?? front.popLast()
}
.popLast() // 2
mutableDeque.popLast() // 1
mutableDeque// [0|]
mutableDeque .popLast() // 0
mutableDeque// [|]
mutableDeque .popLast() // nil mutableDeque
The rest of the Deque was easy, with little to no repetition. Using
protocols in this way was really surprisingly powerful: now, you can
define a DequeType
, with full
access to all of the collection methods, all the way up to RangeReplaceableCollectionType
, in five
lines:
Deque<Element> : DequeType {
public struct var front, back: [Element]
public = DequeSlice<Element>
public typealias SubSequence init() { (front, back) = ([], []) }
public }
<Element> : DequeType {
public struct DequeSlicevar front, back: ArraySlice<Element>
public = DequeSlice
public typealias SubSequence init() { (front, back) = ([], []) }
public }
There’s no performance hit, there’s no safety problems. I only have one version of code to test, one version to change, one version to read. It’s completely extensible: you could use any kind of stack for the front and back. Even another Deque, if you were so inclined:
<Element> : DequeType {
struct DequeDequevar front, back: Deque<Element>
= DequeDequeSlice<Element>
typealias SubSequence init() { front = Deque(); back = Deque() }
}
<Element> : DequeType {
struct DequeDequeSlicevar front, back: DequeSlice<Element>
= DequeDequeSlice
typealias SubSequence init() { front = DequeSlice(); back = DequeSlice() }
}
: DequeDeque = [1, 2, 3, 4, 5, 6, 7, 8]
let dd.front // [4 | 3, 2, 1]
dd.back // [5 | 6, 7, 8] dd
Woo protocols!