Last active
August 26, 2019 18:42
-
-
Save groue/1015bf1679160694430ae95e818ffe2d to your computer and use it in GitHub Desktop.
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
//: # 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