swift-case-paths icon indicating copy to clipboard operation
swift-case-paths copied to clipboard

Case Paths + Key Paths = Optional Paths

Open stephencelis opened this issue 1 year ago • 6 comments

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 avatar Aug 07 '24 21:08 stephencelis

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.

stephencelis avatar Aug 07 '24 22:08 stephencelis

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

JimRoepcke avatar Aug 19 '24 07:08 JimRoepcke

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!

stephencelis avatar Aug 19 '24 16:08 stephencelis

@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

JimRoepcke avatar Aug 19 '24 17:08 JimRoepcke

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

This gives me the following error: image

fruitcoder avatar Dec 03 '24 22:12 fruitcoder

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

stephencelis avatar Dec 23 '24 21:12 stephencelis