Type erasure with multiple adopting types
// Paste me into a playground! | |
import Cocoa | |
//: # Basic Setup | |
protocol FancyProtocol { | |
associatedtype Thing | |
func holdPinkyUp(x: Thing) | |
} | |
struct Dashing: FancyProtocol { | |
func holdPinkyUp(x: String) { print("Dashing: \(x)") } | |
} | |
struct Spiffy: FancyProtocol { | |
func holdPinkyUp(x: String) { print("Spiffy: \(x)") } | |
} | |
//: ## BoxBase | |
//: The base implements the protocol but everything just fatal errors. | |
//: It exists to give us an abstraction we can override later | |
//: And it gives us the template we'll use to "bind" a specific type | |
//: to the associated type of the protocol. | |
class AnyFancyBoxBase<T>: FancyProtocol { | |
func holdPinkyUp(x: T) { | |
//never called | |
fatalError() | |
} | |
} | |
//: ## The Box | |
//: Here we override the BoxBase and specify that our generic parameter | |
//: implements the protocol. This is just a trampoline that forwards | |
//: everything to base. | |
//: | |
//: The key is this type links Base's FancyProtocol.Thing conformance | |
//: to our base class' generic parameter T. | |
final class _FancyBox<Base: FancyProtocol>: AnyFancyBoxBase<Base.Thing> { | |
var base: Base | |
init(_ base: Base) { | |
self.base = base | |
} | |
override func holdPinkyUp(x: Base.Thing) { | |
base.holdPinkyUp(x: x) | |
} | |
} | |
//: ## Type-erased wrapper | |
//: Our type-erased AnyFancy that specifies the base box class, | |
//: | |
//: By using the base we don't have to obey the constraints on _FancyBox | |
//: at the *type* level, only in the initializer. (Otherwise we'd have to | |
//: constrain AnyFancy.T to be FancyProtocol directly) | |
class AnyFancy<T>: FancyProtocol { | |
var _box: AnyFancyBoxBase<T> | |
func holdPinkyUp(x: T) { | |
_box.holdPinkyUp(x: x) | |
} | |
// We constrain the initializer's associated type to match | |
// our generic parameter... basically using the type system | |
// to "pass" a type in to the function. | |
init<U: FancyProtocol>(_ base: U) where U.Thing == T { | |
_box = _FancyBox(base) | |
} | |
} | |
let dashing = Dashing() | |
let spiffy = Spiffy() | |
//: ## Magic | |
//: Our type-erased AnyFancy that specifies the base box class, | |
//: which being a base class frees us from caring about the implementation | |
var anyFancy = AnyFancy(dashing) | |
print("\(type(of: anyFancy))") | |
anyFancy.holdPinkyUp(x: "ok") | |
//: Because Spiffy binds FancyProtocol.Thing to String it is compatible | |
anyFancy = AnyFancy(spiffy) | |
anyFancy.holdPinkyUp(x: "woo") | |
//: ## Further Erasure | |
//: I guess all problems can be solved by another another layer of abstraction? | |
//: | |
//: This is almost identical to our type-erased wrapper except we just lock | |
//: the type parameter to a specific type. | |
class AnyFancyString: FancyProtocol { | |
var _inception: AnyFancy<String> | |
init<U: FancyProtocol>(_ dreamWithinADream: U) where U.Thing == String { | |
_inception = AnyFancy(dreamWithinADream) | |
} | |
func holdPinkyUp(x: String) { | |
_inception.holdPinkyUp(x: x) | |
} | |
} | |
struct Kick { | |
// Look ma, no generics, constraints, or associated types! | |
var anyFancyString: AnyFancyString | |
init(any: AnyFancyString) { | |
self.anyFancyString = any | |
} | |
} | |
let kick = Kick(any: AnyFancyString(anyFancy)) | |
let kick2 = Kick(any: AnyFancyString(dashing)) | |
let kick3 = Kick(any: AnyFancyString(spiffy)) | |
//: # Wake up. | |
let limbo = Kick(any: AnyFancyString(AnyFancyString(AnyFancyString(AnyFancyString(anyFancy))))) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment