Skip to content

Instantly share code, notes, and snippets.

@groue
Last active August 26, 2019 18:42
Show Gist options
  • Save groue/1015bf1679160694430ae95e818ffe2d to your computer and use it in GitHub Desktop.
Save groue/1015bf1679160694430ae95e818ffe2d to your computer and use it in GitHub Desktop.
//: # Toward Enum Key Paths: a Protocol Hierarchy for Read-Only Key Paths
//:
//: This playground is an experiment for a protocol hierarchy of read-only
//: key paths that can handle both throwing and non-throwing getters.
//:
//: Since the Swift language has no support for throwing subscripts, we'll
//: perform our experiments with a very simplified setup that involves a single
//: getter function.
//:
//: ## The Protocol Hierarchy
//:
//: +----------------------------+ +--------------------+
//: | AnyThrowingKeyPathProtocol | -------> | AnyKeyPathProtocol |
//: +----------------------------+ +--------------------+
//: | |
//: | |
//: v v
//: +--------------------------------+ +------------------------+
//: | PartialThrowingKeyPathProtocol | ---> | PartialKeyPathProtocol |
//: +--------------------------------+ +------------------------+
//: | |
//: | |
//: v v
//: +-------------------------+ +-----------------+
//: | ThrowingKeyPathProtocol | ----------> | KeyPathProtocol |
//: +-------------------------+ +-----------------+
//:
//: At the very top of the hierarchy, is the weakest key path.
//: AnyThrowingKeyPathProtocol can handle all types.
protocol AnyThrowingKeyPathProtocol { }
//: We want it to be able to extract an Any? value from a throwing getter.
//:
//: Since we are unable, in this playground, to extend the Any protocol, we
//: define a support protocol:
protocol AnyThrowingKeyPathGettable {
associatedtype Value
func get() throws -> Value
}
//: We also want to make assertions of the various methods we'll use, so we
//: define a handy global.
var lastUsedKeyPathProtocol: String = ""
//: Now we can join the key path and the throwing getter together.
extension AnyThrowingKeyPathGettable {
func get<K: AnyThrowingKeyPathProtocol>(keyPath: K)
throws -> Any?
{
lastUsedKeyPathProtocol = "AnyThrowingKeyPathProtocol"
return try get()
}
}
//: If we put aside throwing getters, we can define AnyKeyPathProtocol, a
//: refinement of AnyThrowingKeyPathProtocol.
protocol AnyKeyPathProtocol: AnyThrowingKeyPathProtocol { }
protocol AnyKeyPathGettable: AnyThrowingKeyPathGettable {
func get() -> Value
}
extension AnyKeyPathGettable {
func get<K: AnyKeyPathProtocol>(keyPath: K)
-> Any
{
lastUsedKeyPathProtocol = "AnyKeyPathProtocol"
return get()
}
}
//: Now it is time to introduce the Root type constraint.
//:
//: PartialThrowingKeyPathProtocol is a refinement of AnyThrowingKeyPathProtocol.
protocol PartialThrowingKeyPathProtocol: AnyThrowingKeyPathProtocol {
associatedtype Root
}
protocol PartialThrowingKeyPathGettable: AnyThrowingKeyPathGettable { }
extension PartialThrowingKeyPathGettable {
func get<K: PartialThrowingKeyPathProtocol>(keyPath: K)
throws -> Any
where K.Root == Self
{
lastUsedKeyPathProtocol = "PartialThrowingKeyPathProtocol"
return try get()
}
}
//: Lets put aside throwing getters again.
protocol PartialKeyPathProtocol: AnyKeyPathProtocol, PartialThrowingKeyPathProtocol { }
protocol PartialKeyPathGettable: AnyKeyPathGettable, PartialThrowingKeyPathGettable { }
extension PartialKeyPathGettable {
func get<K: PartialKeyPathProtocol>(keyPath: K)
-> Any
where K.Root == Self
{
lastUsedKeyPathProtocol = "PartialKeyPathProtocol"
return get()
}
}
//: The next refinement is ThrowingKeyPathProtocol, which adds the typed value.
protocol ThrowingKeyPathProtocol: PartialThrowingKeyPathProtocol {
associatedtype Value
}
protocol ThrowingKeyPathGettable: PartialThrowingKeyPathGettable { }
extension PartialThrowingKeyPathGettable {
func get<K: ThrowingKeyPathProtocol>(keyPath: K)
throws -> Value
where K.Root == Self, K.Value == Self.Value
{
lastUsedKeyPathProtocol = "ThrowingKeyPathProtocol"
return try get()
}
}
//: Lets put aside throwing getters again.
protocol KeyPathProtocol: PartialKeyPathProtocol, ThrowingKeyPathProtocol { }
protocol KeyPathGettable: PartialKeyPathGettable, ThrowingKeyPathGettable { }
extension KeyPathGettable {
func get<K: KeyPathProtocol>(keyPath: K)
-> Value
where K.Root == Self, K.Value == Self.Value
{
lastUsedKeyPathProtocol = "KeyPathProtocol"
return get()
}
}
//: ## Test for Throwing Key Paths
//:
//: Let's define some tests functions around our throwing key paths.
func assertAnyThrowingKeyPathProtocol<O, K, V>(object: O, keyPath: K, expectedValue: V)
throws
where O: AnyThrowingKeyPathGettable, K: AnyThrowingKeyPathProtocol, V: Equatable
{
let value = try object.get(keyPath: keyPath)
assert(value as! V == expectedValue)
assert(lastUsedKeyPathProtocol == "AnyThrowingKeyPathProtocol")
}
func assertPartialThrowingKeyPathProtocol<O, K, V>(object: O, keyPath: K, expectedValue: V)
throws
where O: PartialThrowingKeyPathGettable, K: PartialThrowingKeyPathProtocol, V: Equatable, O == K.Root
{
let value = try object.get(keyPath: keyPath)
assert(value as! V == expectedValue)
assert(lastUsedKeyPathProtocol == "PartialThrowingKeyPathProtocol")
}
func assertThrowingKeyPathProtocol<O, K>(object: O, keyPath: K, expectedValue: O.Value)
throws
where O: ThrowingKeyPathGettable, K: ThrowingKeyPathProtocol, O.Value: Equatable, O == K.Root, O.Value == K.Value
{
let value = try object.get(keyPath: keyPath)
assert(value == expectedValue)
assert(lastUsedKeyPathProtocol == "ThrowingKeyPathProtocol")
}
//: And now run those tests with some concrete throwing types. We need a
//: concrete throwing key path.
struct ThrowingDemoKeyPath<Root, Value>: ThrowingKeyPathProtocol {
typealias Root = Root
typealias Value = Value
}
//: And as a concrete throwing getter, let's use the standard Result enum:
struct KeyPathError: Error { }
extension Result: ThrowingKeyPathGettable { }
do {
let object = Result<Int, Error>.success(1)
let keyPath = ThrowingDemoKeyPath<Result<Int, Error>, Int>()
try assertAnyThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
try assertPartialThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
try assertThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
} catch {
fatalError("\(error)")
}
//: Throwing key paths should also handle non-throwing getters.
struct Object: KeyPathGettable {
var value: Int
func get() -> Int {
return value
}
}
do {
let object = Object(value: 1)
let keyPath = ThrowingDemoKeyPath<Object, Int>()
try assertThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
try assertPartialThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
try assertAnyThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
} catch {
fatalError("\(error)")
}
//: ## Test for Non-Throwing Key Paths
//:
//: We need a concrete non-throwing key path.
struct DemoKeyPath<Root, Value>: KeyPathProtocol {
typealias Root = Root
typealias Value = Value
}
//: Such a key path is also a throwing key path, so the previous tests should
//: perform identically.
do {
let object = Object(value: 1)
let keyPath = DemoKeyPath<Object, Int>()
try assertThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
try assertPartialThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
try assertAnyThrowingKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
} catch {
fatalError("\(error)")
}
//: But we also need non-throwing tests.
func assertAnyKeyPathProtocol<O, K, V>(object: O, keyPath: K, expectedValue: V)
where O: AnyKeyPathGettable, K: AnyKeyPathProtocol, V: Equatable
{
let value = object.get(keyPath: keyPath)
assert(value as! V == expectedValue)
assert(lastUsedKeyPathProtocol == "AnyKeyPathProtocol")
}
func assertPartialKeyPathProtocol<O, K, V>(object: O, keyPath: K, expectedValue: V)
where O: PartialKeyPathGettable, K: PartialKeyPathProtocol, V: Equatable, O == K.Root
{
let value = object.get(keyPath: keyPath)
assert(value as! V == expectedValue)
assert(lastUsedKeyPathProtocol == "PartialKeyPathProtocol")
}
func assertKeyPathProtocol<O, K>(object: O, keyPath: K, expectedValue: O.Value)
where O: KeyPathGettable, K: KeyPathProtocol, O.Value: Equatable, O == K.Root, O.Value == K.Value
{
let value = object.get(keyPath: keyPath)
assert(value == expectedValue)
assert(lastUsedKeyPathProtocol == "KeyPathProtocol")
}
let object = Object(value: 1)
let keyPath = DemoKeyPath<Object, Int>()
assertKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
assertPartialKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
assertAnyKeyPathProtocol(object: object, keyPath: keyPath, expectedValue: 1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment