Skip to content

Instantly share code, notes, and snippets.

@algal
Created December 18, 2015 22:23
Show Gist options
  • Save algal/0bd7874c3c617c8c4add to your computer and use it in GitHub Desktop.
Save algal/0bd7874c3c617c8c4add to your computer and use it in GitHub Desktop.
Example of type-erasing to work with the PAT (Protocol with Associated Type) IntervalType
// Paste me into a playground and then do Editor / Show Rendedered Markup
import UIKit
/*:
Swift defines `IntervalType`, a protocol with associated type (PAT) with one associated type `Bound`
This PAT is adopted by two interval types, `ClosedInterval` and `HalfOpenInterval`
Let's say we want to partition the closed-closed interval 0...1 as a set of intervals
*/
let zeroToHalf = HalfOpenInterval(Float(0), Float(0.5))
let halfToOne = ClosedInterval(Float(0.5),Float(1.0))
//: `let intervals = [zerozero,zeroToHalf,halfToOne]` => error
//: Can't do it, because we can't build a heterogenous collection of different value types adopting IntervalType. In fact, we cannot even define a single variable of type `IntervalType`. Because it must be used in a generic context, we can only define variables of a type that adopts `IntervalType`.
/*:
### Solution 1: Use enums
One alternative approach is to move the variation to the "value level", by using an enumeration with an associated value (an ADT).
This requires us manually to write a `switch` to dispatch over the different kinds of intervals, instead of relying on dynamic dispatch to do the switching.
*/
// poor man's subtype polymorphism
enum IntervalEnum<T:Comparable> {
case ClosedClosed(ClosedInterval<T>)
case ClosedOpen(HalfOpenInterval<T>)
func contains(value:T) -> Bool {
switch self {
case .ClosedClosed(let x): return x.contains(value)
case .ClosedOpen(let x): return x.contains(value)
}
}
}
let intervals3:[IntervalEnum<Float>] = [IntervalEnum.ClosedOpen(zeroToHalf),IntervalEnum.ClosedClosed(halfToOne)]
/*:
### Solution 2: type-erasing wrapper type
In order to create an array of IntervalType<Float>-like objects, we need to define a new type, a *type-erased wrapper for IntervalType*.
This will be a type a that _knows_ the type of `Bound`, but does _not know_ the type of the particular type adopting IntervalType it is based on.
*/
// Abstract class representing IntervalType (so it's generic over Bound)
private class _AnyIntervalBoxBase<MyBound:Comparable> : IntervalType
{
// inferred: typealias Bound = MyBound
func contains(value: MyBound) -> Bool { fatalError("abstract") }
func clamp(intervalToClamp: _AnyIntervalBoxBase) -> Self { fatalError("abstract") }
var isEmpty: Bool { fatalError("abstract") }
var start: MyBound { fatalError("abstract") }
var end: MyBound { fatalError("abstract") }
}
// generic subclass, wrapping the particular type which adopts IntervalType
private final class _AnyIntervalBox<Interval:IntervalType> : _AnyIntervalBoxBase<Interval.Bound> {
let base:Interval
init(_ interval:Interval) {
self.base = interval
}
}
/*:
An (incomplete) type-erased wrapper for IntervalType.
This is generic only over `Bound`, which is the associated type of `IntervalType`.
It is incomplete b/c it does not implement `clamp`, since it's not clear what kind of type clamp should return. We could define it always to return an AnyInterval wrapping a ClosedClosed interval, which would be type-safe but somewhat arbitrary behavior.
Privately, it contains a particular one or another IntervalType type which actually adopts `IntervalType. But it wraps that contained type's identity.
*/
class AnyInterval<MyBound:Comparable> : IntervalType {
// inferred: typealias Bound = MyBound
private let box:_AnyIntervalBoxBase<MyBound>
init<I:IntervalType where I.Bound == MyBound>(someInterval:I) {
self.box = _AnyIntervalBox(someInterval)
}
func contains(value: MyBound) -> Bool { return self.box.contains(value) }
var isEmpty: Bool { return self.box.isEmpty }
var start: MyBound { return self.box.start }
var end: MyBound { return self.box.end }
func clamp(intervalToClamp: AnyInterval) -> Self {
fatalError("whole")
// return self.box.clamp(intervalToClamp)
}
}
/*:
Basically our type-erasing wrapper type is using dynamic dispatch over subtypes, but is hiding that within itself. It might also hide an explicit switch within itself, instead.
*/
/*:
### A Solution That Does Not WOrk: Type-erasing uniform protocol?
Manually a define a uniform protocol for our purposes
*/
protocol IntervalFloat {
func contains(value: Float) -> Bool
// func clamp(intervalToClamp: IntervalFloat) -> IntervalFloat
var isEmpty: Bool { get }
var start: Float { get }
var end: Float { get }
}
/*:
So we what we want is either:
1. to declare that our two specializations HalfOpenInterval<Float> and ClosedInterval<Float> should both adopt `IntervalFloat`; or
2. to declare that all types adopting `IntervalType where Bound==Float` should adopt `IntervalFloat`.
How to do this? Can it be done?
*/
// this will cause only the specialization HalfOpenInterval<Float> to conform to `IntervalFloat`
//: `extension HalfOpenInterval<Float> : IntervalFloat { }` => error
//: No.
//: > Constrained extension must be declared on the unspecialized generic type 'HalfOpenInterval' with constarints specified by a 'where' clause
//: `extension HalfOpenInterval where Bound == Float : IntervalFloat { }` => error
//: No.
//: > Statement cannot begin with a closure expression
//: `extension HalfOpenInterval where Bound:Float : IntervalFloat { }` => error
//: No.
//: > Statement cannot begin with a closure expression
//: `extension HalfOpenInterval:IntervalFloat where Bound:Float { }` => error
//: No.
//: > Extension of type `HalfOpenInterval` with constraints cannot haven an inheritance clause
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment