Last active
May 18, 2022 01:20
-
-
Save russbishop/bb89222f7ef0bd3237aa7f511ededb2f to your computer and use it in GitHub Desktop.
Type erasure with multiple adopting types
This file contains 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! | |
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