-
Notifications
You must be signed in to change notification settings - Fork 134
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
146aa48
785bd55
461962f
b9846bd
7cf22ff
e729c6f
d34879f
ec9533a
c691f7a
44f5fac
28eec12
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,68 @@ | ||
| /// 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) { | ||
| guard casePath.extract(from: $0) != nil else { return } | ||
| $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,135 @@ 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 | ||
|
|
||
| // NB: Force ABI visibility to avoid aggressive downstream WMO pruning | ||
| @usableFromInline | ||
| enum Setter { | ||
| case _embed((Value) -> Any) | ||
| case _set((inout Any, Value) -> Void) | ||
|
|
||
| @usableFromInline | ||
| static func embed<Root>(_ embed: @escaping (Value) -> Root) -> Self { | ||
| ._embed(embed) | ||
| } | ||
|
|
||
| @usableFromInline | ||
| 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>( | ||
| get: { _extract(from: $0)?[keyPath: keyPath] }, | ||
| set: { | ||
| guard var value = _extract(from: $0) else { return } | ||
| value[keyPath: keyPath] = $1 | ||
| _set(into: &$0, value) | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| @_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) | ||
| 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 +262,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,39 +318,27 @@ extension CaseKeyPath { | |
| where Root == Case<Enum>, Value == Case<Void> { | ||
| Case(self)._embed(()) as! Enum | ||
| } | ||
| } | ||
|
|
||
| /// Whether an argument matches the case key path's case. | ||
| /// | ||
| /// ```swift | ||
| /// @CasePathable enum UserAction { | ||
| /// case settings(SettingsAction) | ||
| /// } | ||
| /// @CasePathable enum SettingsAction { | ||
| /// case store(StoreAction) | ||
| /// } | ||
| /// @CasePathable enum StoreAction { | ||
| /// case subscribeButtonTapped | ||
| /// } | ||
| /// | ||
| /// switch userAction { | ||
| /// case \.settings.store.subscribeButtonTapped: | ||
| /// // ... | ||
| /// } | ||
| /// | ||
| /// // Equivalent to: | ||
| /// | ||
| /// switch userAction { | ||
| /// case .settings(.store(.subscribeButtonTapped)): | ||
| /// // ... | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// - Parameters: | ||
| /// - lhs: A case key path. | ||
| /// - rhs: An enum. | ||
| public static func ~= <Enum: CasePathable, AssociatedValue>(lhs: KeyPath, rhs: Enum) -> Bool | ||
| where Root == Case<Enum>, Value == Case<AssociatedValue> { | ||
| rhs[case: lhs] != nil | ||
| extension OptionalKeyPath { | ||
| public func extract<R, V>(from root: R) -> V? where Root == Case<R>, Value == Case<V> { | ||
| Case(self)._extract(from: root) | ||
| } | ||
|
Comment on lines
+324
to
+326
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. Without a |
||
|
|
||
| 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 | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -270,7 +352,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 +362,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 +548,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 +570,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 +589,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. | ||
| /// | ||
|
|
||
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.