pointfreeco / swift-case-paths

🧰 Case paths extends the key path hierarchy to enum cases.
https://www.pointfree.co/collections/enums-and-structs/case-paths
MIT License
904 stars 105 forks source link

Case Paths + Key Paths = Optional Paths #190

Open stephencelis opened 1 month ago

stephencelis commented 1 month ago

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:

@CasePathable
enum Destination {
  case login(Login)
}

struct Login {
  var username = ""
  var password = ""
}

let loginKeyPath: CaseKeyPath<Destination, Login> = \.login

loginKeyPath as OptionalKeyPath  // ✅

let loginPasswordKeyPath: OptionalKeyPath<Destination, String> = \.login.username  // ✅

Some details and experimentation to figure out before merging, so opening as a draft for now.

stephencelis commented 1 month ago

A few other things come to mind:

  1. Is it weird to call the internal thing Case still now that it can refer to non-enum values?
  2. While \Enum.Cases.case made sense for qualifying case key paths, it makes less sense for optional paths: \Struct.Cases.property
  3. It's possible to create an "optional" path from any non-optional key path. This is required for composition to work (\.self paths, as well as \.nonOptionalProperty.optionalProperty). It may feel weird to allow an API that takes optional paths to take a non-optional one, but it also seems the most flexible and we've even had an example in isowords where we wanted to generically ifLet to an optional path in one case and a non-optional path in another.
JimRoepcke commented 3 weeks ago

@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?

stephencelis commented 3 weeks ago

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 :smile:

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!

JimRoepcke commented 3 weeks ago

@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?

image

When I cmd-click on id(state:action:) it takes me here:

image