Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions Sources/CasePaths/AnyOptionalPath.swift
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))>"
}
}
170 changes: 154 additions & 16 deletions Sources/CasePaths/CasePathable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Copy link
Member Author

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.

CaseKeyPath    <R, V> = WritableKeyPath<Case<R>, Case<V>>
OptionalKeyPath<R, V> =         KeyPath<Case<R>, Case<V>>

This empty setter is never really invoked, it's just responsible for ensuring case key paths compose together without losing embed functionality.

}

@_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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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> {
Copy link
Member Author

Choose a reason for hiding this comment

The 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.
Expand All @@ -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> {
Expand All @@ -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)
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Add a set(into:_:) API for partial paths?

}

extension CasePathable {
Expand Down Expand Up @@ -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,
Expand All @@ -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>) {
Expand All @@ -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.
///
Expand Down
5 changes: 5 additions & 0 deletions Sources/CasePaths/Documentation.docc/CasePaths.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@ Do you have a project that uses case paths that you'd like to share? Please
- ``Swift/Result``
- ``Swift/Never``

### Optional paths

- ``OptionalKeyPath``
- ``AnyOptionalPath``

### Migration guides

- <doc:MigrationGuides>
14 changes: 10 additions & 4 deletions Sources/CasePaths/Never+CasePathable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Copy link
Member Author

Choose a reason for hiding this comment

The 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" Case type.

}
}

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 {}
}
}

Expand Down
7 changes: 5 additions & 2 deletions Sources/CasePaths/Optional+CasePathable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ extension Case {
dynamicMember keyPath: KeyPath<Value.AllCasePaths, AnyCasePath<Value, Member?>>
) -> Case<Member>
where Value: CasePathable {
self[dynamicMember: keyPath].some
get {
self[dynamicMember: keyPath].some
}
set {}
}
}

Expand Down Expand Up @@ -91,7 +94,7 @@ extension Optional where Wrapped: CasePathable {
column: UInt = #column
) {
modify(
(\Cases.some).appending(path: keyPath),
(\Cases.some as WritableKeyPath).appending(path: keyPath),
yield: yield,
fileID: fileID,
filePath: filePath,
Expand Down
Loading