I've had this idea for a while but have been distracted by a great many things.
My hand has been forced, a little, by Extract Payload for enum cases having associated value - #140 by gwendal.roue. I consider this a possible alternative. I admit that I've stopped following the thread consistently but I've checked in on the thread every now and then.
Motivation
There are times when you need to construct a decision tree about and enum well before you have an instance of that enum. Consider a state machine and describing valid transitions between states. As a developer, it would be useful to describe, without considering any associated value, which state transitions are valid.
Another benefit of using a protocol is that it provides a consistent way to add more pattern matching capabilities to non-enum types.
The heart of my pitch is:
protocol DiscriminatedUnion {
associatedtype Discriminant : Equatable, CaseIterable // as an aside, it would really be nice if we could mark a protocol as being restricted to enums much like we can with classes. failing that, restricted to types with value semantics.
var discriminant: Discriminant { get }
}
Synthesis for enums marked with DiscriminatedUnion
would look like
enum TrainStatus: DiscriminatedUnion {
case onTime
case early(minutes: UInt)
case delayed(minutes: UInt, reason: String)
case cancelled(String)
}
// Synthesized
extension TrainStatus {
enum Discriminant : Hashable {
case onTime
case early
case delayed
case cancelled
}
var discriminant: Discriminant { … }
}
The way in which this could be an alternative to the proposed payload solution is that one more level of types could be introduced nested inside Discriminant. One type for each case. I haven't settled on a convention for converting labels but a leading underscore should work for now
extension TrainStatus {
extension Discriminant {
enum OnTime {
// might not actually need any cases. Empty enum can serve as a namespace
static func payload(from instance: TrainStatus) -> Void? // this is optional to handle instances that do not match `onTime`
}
enum Early_minutes {
static func payload(from instance: TrainStatus) -> Int?
}
enum Delayed_minutes_reason {
static func payload(from instance: TrainStatus) -> (minutes: UInt, reason: String)?
}
enum Cancelled_reason {
static func payload(from instance: TrainStatus) -> String?
}
}
}
This would allow a state machine to be described (Thanks to @harlanhaskins for this example—which does, in fact, work right now! Try it out!)
protocol DiscriminatedUnion {
associatedtype Discriminator: Equatable where Discriminator: CaseIterable
var discriminant: Discriminator { get }
}
extension DiscriminatedUnion {
func `is`(_ Discriminator: Discriminator) -> Bool {
return discriminant == Discriminator
}
}
struct StateMachine<State: DiscriminatedUnion> where State.Discriminator: Hashable {
enum Error: Swift.Error {
case invalidTransition(from: State, to: State)
}
let transitions: [State.Discriminator: Set<State.Discriminator>]
private(set) var state: State
init(
_ initialState: State,
validTransitions: [State.Discriminator: Set<State.Discriminator>]
) throws {
self.state = initialState
self.transitions = validTransitions
}
mutating func transition(to newState: State) throws {
let states = transitions[state.discriminant]!
guard states.contains(newState.discriminant) else {
throw Error.invalidTransition(from: state, to: newState)
}
state = newState
}
}
enum LoadingState<T>: DiscriminatedUnion {
case notLoaded, loading(Double), loaded(T)
enum Discriminator: Hashable, CaseIterable {
case notLoaded, loading, loaded
}
var discriminant: Discriminator {
switch self {
case .notLoaded: return .notLoaded
case .loading: return .loading
case .loaded: return .loaded
}
}
}
var loadingMachine =
try StateMachine<LoadingState<Int>>(
.notLoaded,
validTransitions: [
.notLoaded: [.loading],
.loading: [.loading, .loaded],
.loaded: []
]
)
try loadingMachine.transition(to: .loading(50))
try loadingMachine.transition(to: .notLoaded)
Future Direction
This could provide a path for improving pattern matching on structs. Either with something along the lines active patterns mentioned by @Joe_Groff or—in certain circumstances—providing a compile time exhaustivity check (when the struct is proven to be composed entirely of types that can be checked for exhaustivity).