-
Notifications
You must be signed in to change notification settings - Fork 29
initial draft for multi-level enums #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d0cda54
4d306b2
8fb9c82
9d268bb
2033663
985438d
70ec127
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
--- | ||
layout: sip | ||
permalink: /sips/:title.html | ||
stage: implementation | ||
status: waiting-for-implementation | ||
presip-thread: https://contributors.scala-lang.org/t/pre-sip-foo-bar/9999 | ||
title: SIP-NN - Multi-Level Enums | ||
--- | ||
|
||
# SIP: Multi-Level Enums | ||
|
||
**By: Raphael Bosshard** | ||
|
||
## History | ||
|
||
| Date | Version | | ||
| ---------- | ------------------ | | ||
| 2025-09-15 | Initial Draft | | ||
|
||
|
||
|
||
## Summary | ||
|
||
This proposal adds minimal syntax to allow enums with nested enumerations, maintaining full exhaustivity checking while keeping syntax clean and intuitive. | ||
|
||
## Motivation | ||
|
||
Scala 3 introduced `enum` as a concise and type-safe way to define algebraic data types. However, it currently supports only flat enums. Many real-world use cases, such as domain modeling or UI state machines, naturally require **hierarchical enums**, where cases are grouped into logical families or categories. | ||
|
||
Consider modeling animals: | ||
|
||
```scala | ||
enum Animal: | ||
case Dog, Cat, Sparrow, Penguin | ||
``` | ||
|
||
This flat structure works, but grouping `Dog` and `Cat` under `Mammal`, and `Sparrow` and `Penguin` under `Bird` is more expressive and enables cleaner abstraction. | ||
|
||
|
||
## Proposed solution | ||
|
||
### High-level overview | ||
|
||
Allow `enum` cases to contain **nested `enum` definitions**, using a consistent indentation-based syntax. | ||
|
||
```scala | ||
enum Animal: | ||
case enum Mammal: | ||
case Dog, Cat | ||
case enum Bird: | ||
case Sparrow, Pinguin | ||
``` | ||
|
||
Each nested `enum` case defines a group of related subcases. The **nested enum is itself a valid subtype** of the parent enum, and its members are **valid cases** of the parent enum, allowing full exhaustivity and pattern matching. | ||
|
||
### Specification | ||
|
||
#### Enum Definition: | ||
|
||
```scala | ||
enum Animal: | ||
case enum Mammal: | ||
case Dog, Cat | ||
case enum Bird: | ||
case Sparrow, Pinguin | ||
case Fish | ||
``` | ||
|
||
- `case enum Mammal:` introduces a **sub-enum case**. | ||
- Nested cases (`Dog`, `Cat`) are **automatically part of the parent enum** (`Animal`), as well as part of the sub-enum (`Mammal`). | ||
|
||
#### Desugaring / Type Relationships | ||
|
||
The above syntax desugars to an enum tree with subtype relationships: | ||
|
||
```scala | ||
sealed trait Animal | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enums are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed. |
||
|
||
object Animal: | ||
sealed abstract class Mammal extends Animal | ||
object Mammal: | ||
case object Dog extends Mammal | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to avoid getting into the weeds, because this is the wrong desugaring, its probably better to explain it in a set of recursive steps, where each There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the pointer! I'll look into that and update the spec. |
||
case object Cat extends Mammal | ||
|
||
sealed trait Bird extends Animal | ||
object Bird: | ||
case object Sparrow extends Bird | ||
case object Pinguin extends Bird | ||
case object FIsh extends Animal | ||
``` | ||
|
||
Results in: | ||
|
||
- `Mammal` and `Bird` are singleton enum cases of `Animal` | ||
- `Dog`, `Cat`, `Sparrow`, and `Pinguin` are **also** values of `Animal`, and they belong to `Mammal` and `Bird` respectively | ||
- Type relationships: | ||
- `Dog <: Mammal <: Animal` | ||
- `Cat <: Mammal <: Animal` | ||
- `Fish <: Animal` | ||
- etc. | ||
|
||
All leaf cases are usable as values of `Animal`, and the nested grouping allows matching at any level of the hierarchy. | ||
|
||
|
||
#### Pattern Matching | ||
|
||
Exhaustive pattern matching on `Animal` must cover all leaf cases: | ||
|
||
```scala | ||
def classify(a: Animal): String = a match | ||
case Dog => "a dog" | ||
case Cat => "a cat" | ||
case Sparrow => "a bird" | ||
case Pinguin => "a penguin" | ||
case Fish => "a fish" | ||
``` | ||
|
||
Matching on intermediate enums is also allowed: | ||
|
||
```scala | ||
def isWarmBlooded(a: Animal): Boolean = a match | ||
case _: Mammal => true // Covers Dog, Cat | ||
case Bird => true // Covers Sparrow, Pinguin | ||
case Fish => false | ||
``` | ||
|
||
Matching on a **supercase type** (e.g., `m: Mammal`) is shorthand for matching all its subcases. | ||
|
||
#### `values`, `ordinal`, `valueOf` | ||
|
||
- `Animal.values` returns all **leaf cases**: `[Dog, Cat, Sparrow, Pinguin, Fish]` | ||
- `Mammal.values` returns `[Dog, Cat]` | ||
- `Mammal.ordinal` and `Mammal.valueOf(...)` are also available | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I missed it, but I think is not explicit how non-leaf ordinals are computed. Would it be? enum Animal: // No ordinal
case enum Mammal: // Ordinal 0
case Dog // Ordinal 0
case Cat // Ordinal 1
case enum Bird: // Ordinal 1
case Sparrow // Ordinal 2
case Pinguin // Ordinal 3
case Fish // Ordinal 2 If that is the case, I assume There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, you didn't miss it. It's currently not specified. Do
|
||
- `Mammal` and `Bird` are usable as enum **values**, but excluded from `values` (unless explicitly included) | ||
|
||
|
||
#### Sealed-ness and Exhaustivity | ||
|
||
- The parent enum and all nested enums are sealed. | ||
- Pattern matching at any level (e.g. on `Mammal`) is **exhaustive** at that level. | ||
- At the top level (`Animal`), exhaustivity means all leaf cases must be covered. | ||
|
||
#### Reflection / Enum APIs | ||
|
||
- `Animal.values`: All leaf values (`Dog`, `Cat`, `Sparrow`, etc.) | ||
- Each nested `case enum` (e.g., `Mammal`) gets its own `.values`, `.ordinal`, and `.valueOf` API. | ||
- `ordinal` value of leaves are global to the supercase (e.g., `Dog.ordinal == 0`, `Cat.ordinal == 1`, etc.) | ||
|
||
#### Syntax Specification (EBNF-like) | ||
|
||
``` | ||
EnumDef ::= 'enum' Id ':' EnumBody | ||
EnumBody ::= { EnumCase } | ||
EnumCase ::= 'case' EnumCaseDef | ||
EnumCaseDef ::= Ids | ||
| 'enum' Id ':' EnumBody | ||
Ids ::= Id {',' Id} | ||
``` | ||
|
||
|
||
#### Compiler | ||
|
||
- The Scala compiler must treat nested enums inside `case enum` as part of the parent enum’s namespace. | ||
- Exhaustivity checking logic must recursively analyze nested enums to extract leaf cases. | ||
- Type relationships must be modeled to reflect subtyping: e.g. `Dog <: Mammal <: Animal`. | ||
|
||
|
||
#### Examples | ||
|
||
#### Example 1: Basic Structure | ||
|
||
```scala | ||
enum Shape: | ||
case enum Polygon: | ||
case Triangle, Square | ||
|
||
case enum Curve: | ||
case Circle | ||
|
||
case Point | ||
``` | ||
|
||
#### Example 2: Moddeling size information | ||
|
||
``` | ||
enum SizeInfo { | ||
case Bounded(bound: Int) | ||
case enum Atomic { | ||
case Infinite | ||
case Precise(n: Int) | ||
} | ||
} | ||
``` | ||
|
||
#### Example 3: Generalized `Either` | ||
|
||
``` | ||
enum AndOr[+A, +B] { | ||
case Both[+A, +B](left: A, right: B) extends AndOr[A, B] | ||
case enum Either[+A, +B] extends AndOr[A, B] { | ||
case Left[+A, +B](value: A) extends Either[A, B] | ||
case Right[+A, +B](value: B) extends Either[A, B] | ||
} | ||
} | ||
|
||
``` | ||
|
||
#### Example 4: | ||
Grouping JSON values into primitives and non-primitives | ||
|
||
``` | ||
enum JsValue { | ||
case Obj(fields: Map[String, JsValue]) | ||
case Arr(elems: ArraySeq[JsValue]) | ||
case enum Primitive { | ||
case Str(str: String) | ||
case Num(bigDecimal: BigDecimal) | ||
case JsNull | ||
case enum Bool(boolean: Boolean) { | ||
case True extends Bool(true) | ||
case False extends Bool(false) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
|
||
|
||
|
||
### Compatibility | ||
- Fully backwards compatible: does not affect existing flat enums. | ||
- Adds optional expressiveness. | ||
- Libraries using `enum` APIs (e.g., `values`) will continue to function with leaf-only views. | ||
- Mirrors and macros | ||
|
||
### Other Concerns | ||
|
||
#### Macro / Tooling Support | ||
|
||
- IDEs and macros need to understand the nested structure. | ||
- Pattern matching hints and auto-completion should support matching on intermediate cases. | ||
|
||
|
||
### Feature Interactions | ||
|
||
|
||
### Alternatives | ||
|
||
- **Flat enums with traits**: more verbose, less exhaustivity checking, more boilerplate. | ||
- **Nested cases with `extends`**: heavier syntax, harder to teach/read. | ||
- **DSLs or macros**: non-standard, cannot integrate with Scala's `enum` semantics cleanly. | ||
|
||
|
||
## Related Work | ||
|
||
|
||
## Faq | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also good to demonstrate how parameters work (e.g.
case enum Bird(val call: String):
) and optional extends clausesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also the rules of extends clauses, e.g. what class is allowed to be extended