I've been thinking of making case
s for struct
s for years now, but for another reason: strong type aliases. The enum
and struct
value types are practically interchangeable with the lack of restrictions on various members that can be defined within them. Having case
s on product types is the only (major?) asymmetry. It's going to be less of a concern after protocols get tweaked to allow case
s to be protocol witnesses for type-level properties/methods; something me and others pushed for since cases and type-level members have the same interaction model.
Hasn't it just been you, and only for the past week? For those that haven't seen the other thread, @Charles_Constant wants enum
instance equivalence to be via the tag only, while we currently expect both the tag and all the payload properties (if any) to be considered.
But we can't have struct
case
s use a different matching philosophy. If they did, then switching an enum
type to a struct
type would be a non-transparent change. Worse, the difference is not syntactic, but only happens at runtime. This wouldn't be acceptable.
So, rethinking on how I would design it....
FIrst, it wouldn't be a protocol, but a new built-in. It's just an extension of an existing declaration:
struct myStruct {
/* Insert the actual members here */
case mySingularCase {
init {
// This is a code block that returns an instance of `Self`.
}
match {
// This is a code block that returns a `Bool`.
// When `true`, a `switch` will use this case.
}
}
case myPayloadCase(Int, whatever: Double) {
init {
// This is a code block that returns an instance of `Self`.
// It can use `$0` and `$1` from the input tuple to create the result.
}
get.0 {
// This is a code block that returns an `Int`.
// It should use the instance's actual properties for the computation.
}
get.1 {
// This is a code block that returns a `Double`.
}
match {
// This is a code block that returns a `Bool`.
}
}
}
"match" is a new contextual keyword. Theoretically, multiple case
s can match; the lexically-first match wins, just like in normal enum
types. The init
and match
blocks are required, while the get.#
blocks are required for each payload member.
I just came up with this for struct
types, but realized that we could let class
types participate in this too. They just need one more required block:
class myClass {
/* Insert the actual members here. */
case myCase(Bool) {
init { /*...*/ }
get.0 { /*...*/ }
match { /*...*/ }
set {
// This is a code block that mutates the appropriate properties of `self`.
// It can use `$0` to determine the new state(s).
}
}
}
For orthogonally, we could support enum
types getting synthetic cases too! (They would support init
/get.#
/match
.)
Synthetic cases can't share the same name. (That's both base name and payload signature; although I think there's still an outstanding bug where cases can't be made that differ only in the payload.) For enum
types, a synthetic case can't share a name with a natural case.
Natural cases must be declared in their type's primary definition. I think keeping that for synthetic cases would leave the primary definition too crowded. I think that we at least should allow synthetic cases to be defined in extension blocks in the primary file too. Maybe in other files of the same module too. Double-maybe in other modules too (if the source type is public, of course). Triple-maybe synthetic cases from outside modules can themselves be publicized.
Synthetic case search is the lexical order in the primary definition first, then lexical order in extensions in the same file (flattening multiple extensions to one list). For other files (if we do that), it's flattened extensions lexical order, but the relative order between files is unspecified. (And same module is considered before outside extensions.)
I came up with match
blocks last. It avoids use from having to make types with synthetic cases to define a instance-level property that returns a token representing something akin to an enum
case tag.
Maybe let payload = myEnumInstance as? case .myCase
?
Here, we need to define a standard library EnumerationCase
type. Its internals are opaque to users. It'll be the return to the global case(of:)
function. This function will let you do tag-only comparisons. Note that once the no-shared-base-name bug for cases is fixed, .foo
and .foo(Int)
will be allowed at the same time, but will be different cases and therefore have distinct tags.