[Pitch] Enum Quality of Life Improvements

Hi. Your task can be solved without language changes, if I understand it correctly.

// Firstly, create this struct: 
@propertyWrapper
public struct EquatableExcluded<T>: Equatable {
  public var wrappedValue: T
  
  public init(wrappedValue: T) {
    self.wrappedValue = wrappedValue
  }
  
  public init(_ wrappedValue: T) {
    self.init(wrappedValue: wrappedValue)
  }
  
  public static func == (lhs: Self, rhs: Self) -> Bool { true }
}

extension EquatableExcluded: Hashable {
  /// Empty Implementation
  public func hash(into hasher: inout Hasher) {}
}

// Then wrap your associated values

enum Navigation: Hashable {
    case firstScreen(EquatableExcluded<FirstScreenData>)
    case secondScreen(EquatableExcluded<SecondScreenData>)
}

There are also some other similar pithes: Comparing enum cases while ignoring associated values - #8 by Dmitriy_Ignatyev

The goal is to have it be equatable at the case level but not at the associated values level, so that solution doesn't work.

If there's other pitches then I'd say it's got traction as something the developers want in the language, or at least those of us willing to pitch it, so perhaps we should see if we can move it along into a real pitch?

You can implement caseName property generically in several lines of code, and then conform Equatable protocol where caseName is compared.

caseName is the least important part of this proposal, I've implemented it myself many times.

IIUC, this is yet another thing that could be implemented in a library if we had structural generic programming. /cc @shabalin

5 Likes

And in an even more general sense, a lot of these things could be unlocked if the reflection capabilities of the language were better. By bringing over more "knowledge" from the compiler onto the language, Swift would enable so many of these capabilities by third parties to flourish — and potentially be added onto the language officially, if uptake is positive.

IMHO reflection is the bottleneck that needs addressing.

2 Likes

Sure: structural generic programming capabilities are reflection. But it is not just any old reflection that's needed—it's reflection that can interact with the type system, which is what you need for synthesizing conformances and associated types. That's what the structural generic programming proposal provides.

6 Likes

The structural generic programming proposal is interesting, but I'd hesitate to say that it's the best way we can expose that information to the type system. Variadic generics will likely improve how these things can be represented - e.g. a struct's members can be represented as a variadic list of parameters, presumably there'd be ways to write extensions and protocol conformances if all parameters met some constraints, etc.

It might actually be happening... :crossed_fingers:

2 Likes

You can use reflection to reproduce that CasePath example. I use this, but it does not make me happy, because explicit typing is required.

In the current language, it probably is.

Variadic generics will likely improve how these things can be represented

Yes, I take it for granted that programming with the kinds of type structures in the structural generic programming proposal would be improved with variadic generics, and I look forward to the introduction of those features with antici… pation. The basic approach is still valid, and could subsume many one-off language extension proposals such as this one, and most ideas for general reflection.

3 Likes

CasePaths used to use reflection, but using a mirror is about 1,000x slower than manually writing an if case let. This may not be a big deal for a casual match/extraction, but can add up quickly in a hot code path. These days CasePaths uses the Swift runtime metadata, instead, and is only about 2–5x slower than an if case let.

As nice as it is to have runtime metadata around to implement this functionality ourselves in a performant-enough way, this really is a feature that should be built into the language, given that key paths are built into the language and are used all over the place these days, like all of the dynamic member lookups in SwiftUI.

21 Likes

I would love to see CasePaths introduced as a language feature (as well as Enum properties). The discrepancy in ergonomics between Structs and Enums is probably the area where I'd most frequently benefit from quality of life improvements, and in my experience a non-negligible source of friction to the more wider adoption of safer, more unambiguous patterns/architectures in Swift codebases.

10 Likes

For reference, I just posted a call to action to try and find a compiler engineer willing to help formalize a pitch for CasePaths.

4 Likes

IMHO, in SwiftUI (and almost every other language in which it's used) Redux-style systems are fine for small apps, but one they reach a certain size you start having to worry about performance issues cropping up on every state change, as making a global state change every single time the user does anything ends up causing the app to diff the entire view tree looking for changes.

Local state, MVVM, and micro-service architectures are much more suited to SwiftUI than Redux.

I know architectures are like religion, but the amount of workarounds that seem to be needed to alleviate your pain points (including requesting changes to Swift itself) seem to me to be a great example of why Redux-style systems are NOT perfect for SwiftUI. It's simply not how it was built.

1 Like

These changes can be motivated equally well in vanilla SwiftUI. As soon as you have enums in your view model, you start needing things like dynamic member lookup for enum cases, which requires closing the key path gap between structs and enums.

9 Likes

You just repeated my own observations about view diffing back at me, and I definitely don’t agree with your logic. I could have said the exact same thing about the actor model that’s currently being shoe-horned into Swift, but languages need to evolve to accomodate new and grander things.

Redux as an architecture provides concrete benefits that others cannot, by virtue of their implicit nature. It’s easy to sweep the reasons for choosing Redux under the rug as “religion” but I’m a pragmatist, not a zealot.

But this post isn’t about Redux, that’s just the biggest pain point for me. Case paths / unassociated enum cases would provide a boon for any Swift engineer, as Stephen said above.

4 Likes

I would like to see case paths with ergonomics as close to key paths as possible, too.

In particular, I would like to be free to use all the features of Swift (like enums) to model my domain in a logical way that avoids invalid states and feel confident I can use those models with things like bindings in SwiftUI, without writing clunky, awkward boilerplate-y code.

While the above is abstract, the Point-Free series on Composable SwiftUI Bindings explains the problems clearly -- with examples -- and outlines how case paths can help solve the problems. If you don't have a subscription (which I highly recommend, fwiw), then the transcript and source code is available to read.

The absence of this is often a glaring and annoying gap. In those situations, an intuition developed with key paths as a Swift developer has lead me to reach for such a tool when none exists.

9 Likes

Bikeshedding the naming: I recommend Discriminator instead of Unassociated, since "discriminator" is the name of the component of an enum case shorn of its associated values.

5 Likes

I use Stripped, since the enums are stripped of associated values. Left only with the case.

I have been coding Stripped versions of enums with associated values for many years and think it would be an amazing addition to the language.

I usually use Sourcery to generate the nested Stripped enum type for me.