-
Notifications
You must be signed in to change notification settings - Fork 131
Case Paths + Key Paths = Optional Paths #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: relax-sendable-remove-reflection-core
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import Foundation | ||
|
|
||
| /// A type-erased optional path that supports extracting an optional value from a root, and | ||
| /// non-optionally updating a value when present. | ||
| /// | ||
| /// This type defines key path-like semantics for optional-chaining. | ||
| public struct AnyOptionalPath<Root, Value> { | ||
| private let _get: (Root) -> Value? | ||
| private let _set: (inout Root, Value) -> Void | ||
|
|
||
| /// Creates a type-erased optional path from a pair of functions. | ||
| /// | ||
| /// - Parameters: | ||
| /// - get: A function that can optionally fail in extracting a value from a root. | ||
| /// - set: A function that always succeeds in updating a value in a root when present. | ||
| public init( | ||
| get: @escaping (Root) -> Value?, | ||
| set: @escaping (inout Root, Value) -> Void | ||
| ) { | ||
| self._get = get | ||
| self._set = set | ||
| } | ||
|
|
||
| /// Creates a type-erased optional path from a type-erased case path. | ||
| /// | ||
| /// - Parameters: | ||
| /// - get: A function that can optionally fail in extracting a value from a root. | ||
| /// - set: A function that always succeeds in updating a value in a root when present. | ||
| public init(_ casePath: AnyCasePath<Root, Value>) { | ||
| self.init(get: casePath.extract) { $0 = casePath.embed($1) } | ||
| } | ||
|
|
||
| /// Attempts to extract a value from a root. | ||
| /// | ||
| /// - Parameter root: A root to extract from. | ||
| /// - Returns: A value if it can be extracted from the given root, otherwise `nil`. | ||
| public func extract(from root: Root) -> Value? { | ||
| self._get(root) | ||
| } | ||
|
|
||
| /// Returns a root by embedding a value. | ||
| /// | ||
| /// - Parameters: | ||
| /// - root: A root to modify. | ||
| /// - value: A value to update in the root when an existing value is present. | ||
| public func set(into root: inout Root, _ value: Value) { | ||
| self._set(&root, value) | ||
| } | ||
| } | ||
|
|
||
| extension AnyOptionalPath where Root == Value { | ||
| /// The identity optional path. | ||
| /// | ||
| /// An optional path that: | ||
| /// | ||
| /// * Given a value to extract, returns the given value. | ||
| /// * Given a value to update, replaces the given value. | ||
| public init() where Root == Value { | ||
| self.init(get: { $0 }, set: { $0 = $1 }) | ||
| } | ||
| } | ||
|
|
||
| extension AnyOptionalPath: CustomDebugStringConvertible { | ||
| public var debugDescription: String { | ||
| "AnyOptionalPath<\(typeName(Root.self)), \(typeName(Value.self))>" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,44 +47,131 @@ public protocol CasePathable { | |
| @_documentation(visibility: internal) | ||
| @dynamicMemberLookup | ||
| public struct Case<Value> { | ||
| fileprivate let _embed: (Value) -> Any | ||
| fileprivate let _extract: (Any) -> Value? | ||
| fileprivate let _get: (Any) -> Value? | ||
| fileprivate let _set: Setter | ||
|
|
||
| fileprivate enum Setter { | ||
| case _embed((Value) -> Any) | ||
| case _set((inout Any, Value) -> Void) | ||
|
|
||
| static func embed<Root>(_ embed: @escaping (Value) -> Root) -> Self { | ||
| ._embed(embed) | ||
| } | ||
|
|
||
| static func set<Root>(_ set: @escaping (inout Root, Value) -> Void) -> Self { | ||
| ._set { | ||
| var root = $0 as! Root | ||
| set(&root, $1) | ||
| $0 = root | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension Case { | ||
| public init<Root>( | ||
| embed: @escaping (Value) -> Root, | ||
| extract: @escaping (Root) -> Value? | ||
| ) { | ||
| self._embed = embed | ||
| self._extract = { ($0 as? Root).flatMap(extract) } | ||
| self.init(get: extract, set: .embed(embed)) | ||
| } | ||
|
|
||
| public init<Root>( | ||
| get: @escaping (Root) -> Value?, | ||
| set: @escaping (inout Root, Value) -> Void | ||
| ) { | ||
| self.init(get: get, set: .set(set)) | ||
| } | ||
|
|
||
| fileprivate init<Root>( | ||
| get: @escaping (Root) -> Value?, | ||
| set: Setter | ||
| ) { | ||
| _get = { ($0 as? Root).flatMap(get) } | ||
| _set = set | ||
| } | ||
|
|
||
| public init() { | ||
| self.init(embed: { $0 }, extract: { $0 }) | ||
| } | ||
|
|
||
| public init<Root>(_ keyPath: CaseKeyPath<Root, Value>) { | ||
| public init<Root>(_ keyPath: OptionalKeyPath<Root, Value>) { | ||
| self = Case<Root>()[keyPath: keyPath] | ||
| } | ||
|
|
||
| public subscript<AppendedValue>( | ||
| dynamicMember keyPath: KeyPath<Value.AllCasePaths, AnyCasePath<Value, AppendedValue>> | ||
| ) -> Case<AppendedValue> | ||
| where Value: CasePathable { | ||
| get { | ||
| let casePath = Value.allCasePaths[keyPath: keyPath] | ||
| var set: Case<AppendedValue>.Setter { | ||
| switch _set { | ||
| case ._embed(let embed): | ||
| return .embed { embed(Value.allCasePaths[keyPath: keyPath].embed($0)) } | ||
| case ._set(let set): | ||
| return .set { set(&$0, casePath.embed($1)) } | ||
| } | ||
| } | ||
| return Case<AppendedValue>( | ||
| get: { _extract(from: $0).flatMap(casePath.extract) }, | ||
| set: set | ||
| ) | ||
| } | ||
| set {} | ||
| } | ||
|
|
||
| @_disfavoredOverload | ||
| public subscript<AppendedValue>( | ||
| dynamicMember keyPath: WritableKeyPath<Value, AppendedValue> | ||
| ) -> Case<AppendedValue> { | ||
| Case<AppendedValue>( | ||
| embed: { _embed(Value.allCasePaths[keyPath: keyPath].embed($0)) }, | ||
| extract: { _extract(from: $0).flatMap(Value.allCasePaths[keyPath: keyPath].extract) } | ||
| get: { _extract(from: $0)?[keyPath: keyPath] }, | ||
| set: { | ||
| guard var value = _extract(from: $0) else { return } | ||
| value[keyPath: keyPath] = $1 | ||
| _set(into: &$0, value) | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| public func _embed(_ value: Value) -> Any { | ||
| self._embed(value) | ||
| @_disfavoredOverload | ||
| public subscript<AppendedValue>( | ||
| dynamicMember keyPath: WritableKeyPath<Value, AppendedValue?> | ||
| ) -> Case<AppendedValue> { | ||
| Case<AppendedValue>( | ||
| get: { _extract(from: $0)?[keyPath: keyPath] }, | ||
| set: { | ||
| guard var value = _extract(from: $0) else { return } | ||
| value[keyPath: keyPath] = $1 | ||
| _set(into: &$0, value) | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| public func _embed(_ value: Value) -> Any? { | ||
| switch _set { | ||
| case ._embed(let embed): | ||
| return embed(value) | ||
| case ._set: | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| public func _extract(from root: Any) -> Value? { | ||
| self._extract(root) | ||
| _get(root) | ||
| } | ||
|
|
||
| public func _set(into root: inout Any, _ value: Value) { | ||
| switch _set { | ||
| case ._embed(let embed): | ||
| root = embed(value) | ||
| case ._set(let set): | ||
| set(&root, value) | ||
| } | ||
| } | ||
|
|
||
| private struct Unembeddable {} | ||
| } | ||
|
|
||
| private protocol _AnyCase { | ||
|
|
@@ -171,7 +258,10 @@ extension Case: _AnyCase { | |
| /// identity case key path `\SomeEnum.Cases.self`. It refers to the whole enum and can be passed to | ||
| /// a function that takes case key paths when you want to extract, change, or replace all of the | ||
| /// data stored in an enum in a single step. | ||
| public typealias CaseKeyPath<Root, Value> = KeyPath<Case<Root>, Case<Value>> | ||
| public typealias CaseKeyPath<Root, Value> = WritableKeyPath<Case<Root>, Case<Value>> | ||
|
|
||
| /// A key path to a writable, optional-chained value. | ||
| public typealias OptionalKeyPath<Root, Value> = KeyPath<Case<Root>, Case<Value>> | ||
|
|
||
| extension CaseKeyPath { | ||
| /// Embeds a value in an enum at this case key path's case. | ||
|
|
@@ -224,7 +314,9 @@ extension CaseKeyPath { | |
| where Root == Case<Enum>, Value == Case<Void> { | ||
| Case(self)._embed(()) as! Enum | ||
| } | ||
| } | ||
|
|
||
| extension OptionalKeyPath { | ||
| /// Whether an argument matches the case key path's case. | ||
| /// | ||
| /// ```swift | ||
|
|
@@ -254,10 +346,33 @@ extension CaseKeyPath { | |
| /// - Parameters: | ||
| /// - lhs: A case key path. | ||
| /// - rhs: An enum. | ||
| public static func ~= <Enum: CasePathable, AssociatedValue>(lhs: KeyPath, rhs: Enum) -> Bool | ||
| public static func ~= <Enum: CasePathable, AssociatedValue>( | ||
| lhs: KeyPath, | ||
| rhs: Enum | ||
| ) -> Bool | ||
| where Root == Case<Enum>, Value == Case<AssociatedValue> { | ||
| rhs[case: lhs] != nil | ||
| } | ||
|
|
||
| public func extract<R, V>(from root: R) -> V? where Root == Case<R>, Value == Case<V> { | ||
| Case(self)._extract(from: root) | ||
| } | ||
|
|
||
| public func set<R, V>(into root: inout R, _ value: V) where Root == Case<R>, Value == Case<V> { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New functionality. Worth bike-shedding the API or good as is? |
||
| var anyRoot = root as Any | ||
| Case(self)._set(into: &anyRoot, value) | ||
| root = anyRoot as! R | ||
| } | ||
|
|
||
| public func modify<R, V>(into root: inout R, yield: (inout V) -> Void) | ||
| where Root == Case<R>, Value == Case<V> { | ||
| var anyRoot = root as Any | ||
| let `case` = Case(self) | ||
| guard var value = `case`._extract(from: anyRoot) else { return } | ||
| yield(&value) | ||
| `case`._set(into: &anyRoot, value) | ||
| root = anyRoot as! R | ||
| } | ||
| } | ||
|
|
||
| /// A partially type-erased key path, from a concrete root enum to any resulting value type. | ||
|
|
@@ -270,7 +385,7 @@ extension _AppendKeyPath { | |
| /// type, the operation will fail. | ||
| /// - Returns: An enum for the case of this key path that holds the given value, or `nil`. | ||
| @_disfavoredOverload | ||
| public func callAsFunction<Enum: CasePathable>( | ||
| public func callAsFunction<Enum>( | ||
| _ value: Any | ||
| ) -> Enum? | ||
| where Self == PartialCaseKeyPath<Enum> { | ||
|
|
@@ -280,6 +395,10 @@ extension _AppendKeyPath { | |
| } | ||
| return _openExistential(value, do: open) | ||
| } | ||
|
|
||
| public func extract<R>(from root: R) -> Any? where Self == PartialCaseKeyPath<R> { | ||
| (Case<R>()[keyPath: self] as? any _AnyCase)?.extractAny(from: root) | ||
| } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| extension CasePathable { | ||
|
|
@@ -462,7 +581,7 @@ extension CasePathable { | |
| /// - line: The line where the modify occurs. | ||
| /// - column: The column where the modify occurs. | ||
| public mutating func modify<Value>( | ||
| _ keyPath: CaseKeyPath<Self, Value>, | ||
| _ keyPath: OptionalKeyPath<Self, Value>, | ||
| yield: (inout Value) -> Void, | ||
| fileID: StaticString = #fileID, | ||
| filePath: StaticString = #filePath, | ||
|
|
@@ -484,12 +603,14 @@ extension CasePathable { | |
| return | ||
| } | ||
| yield(&value) | ||
| self = `case`._embed(value) as! Self | ||
| var anySelf = self as Any | ||
| `case`._set(into: &anySelf, value) | ||
| self = anySelf as! Self | ||
| } | ||
| } | ||
|
|
||
| extension AnyCasePath { | ||
| /// Creates a type-erased case path for given case key path. | ||
| /// Creates a type-erased case path for a given case key path. | ||
| /// | ||
| /// - Parameter keyPath: A case key path. | ||
| public init(_ keyPath: CaseKeyPath<Root, Value>) { | ||
|
|
@@ -501,6 +622,23 @@ extension AnyCasePath { | |
| } | ||
| } | ||
|
|
||
| extension AnyOptionalPath { | ||
| /// Creates a type-erased optional path for a given optional key path. | ||
| /// | ||
| /// - Parameter keyPath: An optional key path. | ||
| public init(_ keyPath: OptionalKeyPath<Root, Value>) { | ||
| let `case` = Case(keyPath) | ||
| self.init( | ||
| get: { `case`._extract(from: $0) }, | ||
| set: { | ||
| var anyRoot = $0 as Any | ||
| `case`._set(into: &anyRoot, $1) | ||
| $0 = anyRoot as! Root | ||
| } | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| extension AnyCasePath where Value: CasePathable { | ||
| /// Returns a new case path created by appending the case path at the given key path to this one. | ||
| /// | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,16 +16,22 @@ extension Case where Value: CasePathable { | |
| /// This property can chain any case path into a `Never` value, which, as an uninhabited type, | ||
| /// cannot be embedded nor extracted from an enum. | ||
| public var never: Case<Never> { | ||
| func absurd<T>(_: Never) -> T {} | ||
| return Case<Never>(embed: absurd, extract: { (_: Value) in nil }) | ||
| get { | ||
| func absurd<T>(_: Never) -> T {} | ||
| return Case<Never>(embed: absurd, extract: { (_: Value) in nil }) | ||
| } | ||
| set {} | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These sets are to preserve case key path functionality. Luckily the weirdness is all hidden away in the "internal" |
||
| } | ||
| } | ||
|
|
||
| extension Case { | ||
| @available(*, deprecated, message: "This enum must be '@CasePathable' to enable key path syntax") | ||
| public var never: Case<Never> { | ||
| func absurd<T>(_: Never) -> T {} | ||
| return Case<Never>(embed: absurd, extract: { (_: Value) in nil }) | ||
| get { | ||
| func absurd<T>(_: Never) -> T {} | ||
| return Case<Never>(embed: absurd, extract: { (_: Value) in nil }) | ||
| } | ||
| set {} | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type system now tracks case key paths as writable key paths, and optional paths as plain ole key paths.
This empty setter is never really invoked, it's just responsible for ensuring case key paths compose together without losing embed functionality.