Created
December 18, 2015 22:23
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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