-
Notifications
You must be signed in to change notification settings - Fork 110
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
Case Paths + Key Paths = Optional Paths #190
base: main
Are you sure you want to change the base?
Conversation
} | ||
) | ||
} | ||
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.
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.
public func extract(from root: Any) -> Value? { | ||
self._extract(root) | ||
self._get(root) | ||
} |
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.
There's a bit of a mismatch in this PR where sometimes we say "extract" and sometimes we say "get" (mostly just in the initializer for symmetry with "set"). Not sure if we want to consider something else.
public func extract<R, V>(from root: R) -> V? where Root == Case<R>, Value == Case<V> { | ||
Case(self).extract(from: root) | ||
} |
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.
Without a CasePathable
requirement to tie a [case:]
subscript to, we need a way to utilize this functionality more generally.
Case(self).extract(from: root) | ||
} | ||
|
||
public func set<R, V>(into root: inout R, _ value: V) where Root == Case<R>, Value == Case<V> { |
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.
New functionality. Worth bike-shedding the API or good as is?
|
||
public func extract<R>(from root: R) -> Any? where Self == PartialOptionalKeyPath<R> { | ||
(Case<R>()[keyPath: self] as? any _AnyCase)?._extract(from: root) | ||
} |
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.
- Add a
set(into:_:)
API for partial paths?
@Sendable 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.
These sets are to preserve case key path functionality. Luckily the weirdness is all hidden away in the "internal" Case
type.
A few other things come to mind:
|
@stephencelis I was using the old OptionalPath code in a project that was on TCA 1.10.4 and swift-case-paths 1.3.0. Trying to migrate it to TCA 1.13.0, and had some compilation errors due to deprecations with case paths. Just stumbled across this PR, what great timing! Just curious if you know what the timing might be for getting this merged and released? |
Hi @JimRoepcke! Theoretically this branch is ready to go, but we haven't really prioritized it. In most of our projects we've remodeled our domains in ways that no longer require optional paths, and so the main thing needed for this branch to land is some beta testers, which is where you could come in 😄 Want to point your project at this branch and take things for a spin to see if it unblocks you? We're eager for feedback from users with real world requirements! |
@stephencelis I pointed my code at this branch. Working through issues in my code, but I'm also seeing TCA (1.13.0) itself is not building? When I cmd-click on |
Hi @stephencelis, will this also fix an issue I had when trying to model my domain by wrapping my enum reducer in another enum before putting it in State? The code can be simplified to this @CasePathable
enum LoadingState: Equatable {
case loading
case loaded(ContentReducer.State)
}
@Reducer(state: .equatable)
enum ContentReducer {
case child1(Child1Reducer.State)
}
@Reducer
struct Child1Reducer {
typealias State = Never
typealias Action = Never
typealias Body = EmptyReducer
}
@Reducer
struct Child2Reducer {
typealias State = Never
typealias Action = Never
typealias Body = EmptyReducer
}
@Reducer
struct Feature {
@ObservableState
struct State: Equatable {
var content: LoadingState = .loading
}
enum Action {
case content(ContentReducer.Action)
}
public var body: some ReducerOf<Self> {
EmptyReducer()
.ifLet(\.content.loaded, action: \.content) {
ContentReducer.body
}
}
} |
@fruitcoder Technically it should, though TCA would need to update all its helpers to work with optional paths rather than key paths and case paths. |
This PR adds some basic support for case/key path composition via "optional" paths.
Often there are case path APIs that don't need the full power of a case path. You might need to extract an optional value, or update an existing optional value, but you don't need to embed that value into a whole new root. This is the behavior of optional-chaining in Swift, which is sadly unsupported when it comes to key paths: an optional-chained key path loses writability.
This PR changes that by expanding the functionality of case key paths so that they gracefully degrade to an "optional" key path.
For example:
Some details and experimentation to figure out before merging, so opening as a draft for now.