Skip to content

Instantly share code, notes, and snippets.

@russbishop
Last active May 18, 2022 01:20
Show Gist options
  • Save russbishop/bb89222f7ef0bd3237aa7f511ededb2f to your computer and use it in GitHub Desktop.
Save russbishop/bb89222f7ef0bd3237aa7f511ededb2f to your computer and use it in GitHub Desktop.
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