We're running a small poll about the potential syn...
# language-evolution
a
We're running a small poll about the potential syntax of a new "incomplete enum/sealed classes" feature, and would love to hear from you! forms.gle/… Feel free to share the poll around, the more responses we have, the merrier πŸ™‚
πŸ‘€ 3
m
Maybe stupid question but do we really need the
else? ->
part? Can't the compiler infer from the declaration that the enum is incomplete and in that case allow the regular
else ->
syntax without a warning? (And make a warning if it's not there?)
a
The
else?
covers a slightly different use case: when you want your code to actually break if the enum/sealed class gets a new value. That is, with
else?
the rest of the branches must still be exhaustive.
πŸ‘πŸΌ 1
πŸ‘ 1
πŸ‘πŸ» 1
m
Ah, yes of course
a
In other words, 99% of the time you use
enum?
you should expect users to use
else
, not
else?
, since they should not care about you adding new members (that's what this is all about). But we have a subset of cases where this exhaustiveness is somehow advisable (e.g., a utils library accompanying a core library, where both co-evolve)
m
99% of the time
Shouldn't I want
else? ->
most of the time though? If I bump my library dependency and a new value comes up, I want to be made aware of it and press [Alt + Enter] to introduce the new case, which I understand is only possible with
else? ->
?
πŸ’― 3
a
Not really. If you expect your users to update their code whenever a new case arises, then this is not an incomplete enum, but an enum with a breaking change. A good example of an incomplete enum would be Key in Compose, in which you normally handle only a few keys, and don't care if the enum is expanded in the future
m
breaking change
We were discussing this just this morning πŸ™‚ I wouldn't necessarily say it's a breaking change or else everything can be described as a breaking change. My use case is I have sealed classes representing GraphQL AST nodes. When new syntax is introduced, there are new nodes but I wouldn't describe it as a breaking change though. You can keep parsing the older documents and it's working the same. Now of course if you want to support the newer syntax, you'll need the new AST nodes so I'd use
else? ->
in that case
b
What about having an annotation for that instead? β€’ β€œI don’t care about new values” case:
else
is mandatory for an incomplete enum β€’ β€œI want to break if there are new values” case: add e.g.
@ForceExhaustive
on the
when
which disallows
else
m
I want to break if there are new values
It's not just that I think. It's "I want to break if there are new build time values but still have a fallthrough for 'unknown at build time' values"
β€’
else
catches everything β€’
else?
catches everything but errors at build time if there are unhandled cases known at build time
a
I'd love if at this point we could keep the conversation focused on the syntax. The design is still in flux, so new options may arise, but knowing the preferred syntax would help us showcase this better to the community
b
I think I get that (not 100% sure πŸ˜…) - but maybe disallowing
else
for the second case is a good trade-off (vs introducing new syntax). What I mean: if you want to handle incomplete enums, and break when there are new values -> you can't can't have a fallthrough
else
in your
when
, you must list all values.
@Alejandro Serrano.Mena in that case I would just say that my preference would be to introduce no new syntax ( if possible of course, for instance annotations).
m
keep the conversation focused on the syntax
Just want to make sure I understand what the proposed syntax is doing ^^
a
@bod you need new syntax if you care to distinguish the cases of: β€’ "this branch catches all remaining cases" => this has to be the current
else
to preserve binary compatibility β€’ "this branch applies to future-and-unknown cases" Our preliminary investigation shows that some people care about that distinction, and this is why we are thinking of a new form of
else
m
I think I want
else?
myself but I feel weird being in the 1% πŸ™‚
βœ‹ 1
βœ… 1
h
For me
?
already means nullability in Kotlin. String?, ?., ?: all mean "might be null." enum? reads as "nullable enum" and else? reads as "optional else" or "matches null." Neither reading is correct in my head Kotlin has been consistent about ? = null, and this would break that no? else? signals the opposite of what it does. A (in the form)? suggests something looser but else? is the stricter form of it as it makes the compiler require every known case Might be a good idea to lean on how Swift does thinfs: such as
else unknown -> (Swift precedent: @unknown default), or else future ->
What about
Copy code
nonexhaustive enum class Version { V1, V2, V3 }

when (v) {
    V1 -> "original"
    V2 -> "refined"
    V3 -> "awesome"
    else unknown -> "unknown"
}
Then it would read as
Copy code
switch v {
case .v1: "original"
case .v2: "refined"
case .v3: "awesome"
@unknown default: "unknown"
}
In Swift interop too? Not sure about nonexhaustive word just threw it out there as an idea
a
@Hristijan do you mind leaving your thoughts also in the poll? otherwise it gets very difficult for me to account from different sources
h
Do we need to wait do the results or could we see them directly/live?
But I also like the syntax similar to Swift open enum class else -> { known at compile time, and existing fallback } unknown -> { unknown at compile time }
πŸ‘ 1
plus one 4
πŸ‘πŸΌ 1
e
I guess
open enum class
is not too bad, but I think an annotation would be better for compatibility. no new keywords, existing parsers (eg ktlint) continue working, can be produced by existing formatters (eg kotlinpoet)
Copy code
@NonExhaustive enum class Version
on the other side, I'd rather see
Copy code
when (this) {
    else -> unreachable()
}
where the compiler will warn or error if the code is reachable. similarly better for compatibility, and also potentially more generally useful as well
βž• 2
r
I somewhat like
open
for a modifier since it's familiar and seems pretty analogous to existing usage. But if we're applying this to sealed classes as well as enums then we'd have
open sealed
declarations which could be confusing. We do already have
internal external
declarations, so there's precedent for combining modifiers in Kotlin that sound like opposites in English, but that still might not be something we want more of.
p
I read
open enum
as "someone else can extend this" which isn't the intent here.
agree 6
☝️ 1
z
open enum
would be passible but I think
open sealed
would not. What about
extensible
+
_
? With the underscore as an alternative to a new
unknown
keyword
Copy code
extensible enum class Version { V1, V2, V3 }

fun Version.render(): String = when (this) {
    Version.V1 -> "original"
    Version.V2 -> "refined"
    Version.V3 -> "awesome"
    else _ -> "unknown"
}
iirc zig supports the
_
else option
swift has some good writing on why they decided against some of the keywords discussed above: github.com/swiftlang/swift-evolution/blob/…/0192-non-exhaustive-enums.md#…
c
I definitely thing it shouldn't be a symbol, because that'd be one more thing to learn; and it definitely shouldn't be
?
, because this feature has nothing to do with nullability. I kinda like
else unknown ->
plus one 1
I also don't like
open enum
.
else incomplete ->
is ok, but
incomplete enum
isn't beautiful.
Copy code
extensible enum class …

else extended β†’
It's a bit too similar to Java's
extends
, but I doubt users would find it confusing since enums can't be part of hierarchies anyway
Could it be possible to edit the form so we can edit our answers?
Copy code
when (…) {
    Foo.One ->
    Foo.Two ->
    else if extended ->
}
I think that ties a bit better with what we're used to write πŸ€” that's a breaking change if the user has a variable called
extended
though, right? unless the variable is given priority over the keyword, but that's a bit strange
z
the more I think on it the more I strongly feel the underscore in the else branch is the natural fit. I care less about what keyword/annotation is used on the enum/sealed class itself, but the underscore feels right and a natural extension within the language
Copy code
extensible enum class Version { V1, V2, V3 }

fun Version.render(): String = when (this) {
    Version.V1 -> "original"
    Version.V2 -> "refined"
    Version.V3 -> "awesome"
    else _ -> "unknown"
}
it also captures well that you don't know what it is
πŸ‘ 1
c
I do like
else _
but it's easy to think it means "all other cases" whereas it means "all cases that appear at runtime without being known at compile time" I agree it aesthetically fits into the language but I'm worried it sends the wrong message about what it does
p
I think
else unknown ->
is clear -
_
makes me think of pattern matching
βž• 1
z
I think most of my hesitation with that is that it adds another keyword to the language, and in this case for exactly one narrow use case
re: misleading use - I'm not sure I agree, it seems to work fine in zig
c
(somewhat related, but Kotlin keywords definitely should have quick documentation in the IDE explaining what they do, we do have many soft-keyword/modifiers that the average dev will see maybe once)
βž• 1
πŸ‘Œ 2
plus one 1
I don't write Zig, but I can't find a Zig equivalent of the new proposed
else
variant? github.com/ziglang/zig/issues/2524 just describes a regular
else
Edit, found it: https://www.reddit.com/r/Zig/comments/ttucl3/what_it_a_nonexhaustive_enum/
l
What about
else! ->
? The exclamation mark suffix is already used in Kotlin to refer to something unknown: the nullability for an unannotated Java type (platform types), in the IDE. To me, it'd make sense to also use it for unknown other enum cases. It also speaks less about nullability than
else? ->
to me, being unrelated to known nullability, and the elvis operator, which has the question mark as a suffix to
:
.
❀️ 1
c
!!
is also about nullability, and
Foo!
is not really a part of the language, it can't appear in source files Also Java is considering
String!
for non-nullable types But I agree that
Foo!
in Kotlin means "the compiler can't check something", which matches quite well with "the compiler is afraid of new members added in the future"
r
else!
makes me think it might crash if it sees something unexpected, when this is really the opposite.
l
There could be an
else!!
to actually make it crash? πŸ™ƒ
p
Or
else?!
for when it's truly supposed to be exhaustive.
z
I can't be the only person that thinks words with characters in them are harder to read.
?
has high enough value prop + it's usually the last character in that expression,
else! ->
feels like a lexical rollercoaster
βž• 2
d
I'm a bit late to the party, but I think we think the same Zac, I just submitted a response to the form saying I like using
_
. I may be incorrect but I think you could skip the
else
altogether in that case because I don't think it's a currently a valid variable name in that position.
Copy code
fun Version.render(): String = when (this) {
    Version.V1 -> "original"
    Version.V2 -> "refined"
    Version.V3 -> "awesome"
    _ -> "unknown"
}
Seems better to me because you don't have the issue of someone accidentally missing the modifier and doing
else -> { }
, disabling the exhaustiveness check. I just saw the Zig version and I like how it uses it. It's basically the pseudocode I'd write if I was partially defining an enum in a design doc or something.