language icon indicating copy to clipboard operation
language copied to clipboard

Define local enums

Open filiph opened this issue 6 months ago • 5 comments

I'd love to be able to define local enums the same way I can define local functions or variables. Like so:

class Blah {

  // ... a lot of code ...

  Result process(Input input, bool significant) {
    // This enum is only useful in this method. I don't need to put it all the way to the outside of the class.
    enum Semaphore { red, orange, green }

    final Semaphore type;
    if (input.signal >= 3.5) {
      type = Semaphore.red;
    } else if (input.mood) {
      type = Semaphore.orange; 
    } else {
      type = Semaphore.green;
    }

    // Use exhaustive switch statements etc.
  }

  // ... a lot of code (rest of class) ...

}

I think enums are great. They are able to make intents much clearer, and with exhaustive switch statements, they guard against several types of bugs (forgotten cases). I also like to extract an enum first (like above) and then use its value several times (e.g. having two separate switch statements, one using the value alone, another in addition to another value).

Inside local function scope, we can of course use bool (a kind of enum, if you think about it) and also any enums that are defined outside the class, but you can't just create a local enum. If you have something more than a binary true/false for decision, you have to either:

  1. Forget about exhaustiveness checking and do things like else if (foo == 3)
  2. Try to use several boolean values (e.g. if (bald && canSing)) -- but that's not always possible or desirable
  3. Create a new enum outside the class

The last option adds friction. The new enum, however small, now sits many pages of code away. So it's not right there for the programmer to see. It also pollutes the scope for the whole library despite being a one-off. So now you have some kind of _SemaphoreProcessingType at the end of the file, and just by reading it, you don't know where it's used.

filiph avatar Jun 10 '25 12:06 filiph

Here's a real world example (the one I give above is optimized for ease of reading but maybe doesn't really sell the problem).

This is the code that I currently need to write if I want exhaustiveness checking in a method:

class CollisionSystem {

  // ... more code ...

 Sfx? _chooseAppropriateSoundEffect(Collision collision) {
    final projectile = collision.involvedProjectile;
    if (projectile == null) {
      // Two non-projectile entities meet.
      // TODO: deal with other options:
      //       - vehicle colliding with rock
      //       - vehicle colliding with structure / house
      return Sfx.twoVehiclesCollide;
    }

    final _ProjectileType projectileType;
    if (projectile.tags.contains(Tag.isAutocannonRound)) {
      projectileType = _ProjectileType.cannonRound;
    } else if (projectile.tags.contains(Tag.isCannonRound)) {
      projectileType = _ProjectileType.gunRound;
    } else {
      projectileType = _ProjectileType.other;
    }

    final target = projectile == collision.a ? collision.b : collision.a;
    final _TargetType targetType;
    if (target.tags.contains(Tag.isMasonry)) {
      targetType = _TargetType.masonry;
    } else if (collision.involvesPlayer) {
      targetType = _TargetType.playerHull;
    } else {
      targetType = _TargetType.hull;
    }

    switch ((projectileType, targetType)) {
      case (_ProjectileType.cannonRound, _TargetType.playerHull):
        return Sfx.cannonRoundHitsPlayerHull;
      case (_ProjectileType.gunRound, _TargetType.playerHull):
        return Sfx.gunRoundHitsPlayerHull;
      case (_ProjectileType.other, _TargetType.playerHull):
        return Sfx.projectileHitsPlayerHull;

      case (_ProjectileType.cannonRound, _TargetType.hull):
        return Sfx.cannonRoundHitsHull;
      case (_ProjectileType.gunRound, _TargetType.hull):
        return Sfx.gunRoundHitsHull;
      case (_ProjectileType.other, _TargetType.hull):
        return Sfx.projectileHitsHull;

      case (_ProjectileType.cannonRound, _TargetType.masonry):
        return Sfx.cannonRoundHitsGround;
      case (_ProjectileType.gunRound, _TargetType.masonry):
        return Sfx.gunRoundHitsGround;
      case (_ProjectileType.other, _TargetType.masonry):
        return Sfx.projectileHitsGround;
    }
  }

  // ... more code ...

}

enum _ProjectileType { cannonRound, gunRound, other }

enum _TargetType { hull, playerHull, masonry }

filiph avatar Jun 10 '25 12:06 filiph

The DAS team uses a lot of the existing functionality (private enums) for creating assists/fixes.

It would be really great to see this and local extensions (which I suggested at the original namespace issue).

FMorschel avatar Jun 10 '25 19:06 FMorschel

I'm not morally opposed to this, but my impression from poking around Rust discussions over the years is that supporting locally defined types adds more complexity to the compiler than you might expect. It means, at the very least, that you don't know all of the types a module is working with until you parse and resolve not just top level code but method bodies too.

It also raises questions around what happens if the type escapes:

Object f() {
  enum E { e }
  return E.e;
}

Is this allowed? It's probably no worse than having a private type returned from a public API, but we'd want to make sure that's actually true before we commit to it.

Here's another fun case:

f<T>() {
  enum E {
    e;
    method(T t) {}
  }
}

Is that valid?

munificent avatar Jun 13 '25 23:06 munificent

FWIW, I think it's very much okay to limit these local enums to "dumb" enums. No methods, conductors or static members. Just values.

I would also expect Dart to prevent these local enums from escaping (or making the behavior undefined, e.g. maybe returning the underlying representations such as uint).

filiph avatar Jun 15 '25 15:06 filiph

I see no reason to limit local enums to not having members, or not being returnable. (Dart enums do not have an "underlying representations", they are full objects with methods.)

Instead they should be restricted to not accessing the dynamic scope, as if they were static declarations inside a class. That means that they could have been declared as top level declarations, and the only difference between a top level enum and a local enum is the scope of their name (which is also not in the export scope then).

We could require you to write

  static enum Foo {…}

for a nested or local enum. That would allow us to later allow non -static declarations too, like Java inner classes.

No need to stop at enums, we can allow static nested or local classes too, or any other top-level declaration. (Or just typedefs, then you can bring any of the other into scope, except extensions.)

lrhn avatar Jun 16 '25 06:06 lrhn