How to: exhaustive enum case

Suppose you want to choose a code path depending on a variable value. You could use a case for that:

case value
  when :one then p one: value
  when :two then p two: value
end

This has several problems, one of which is this: what’s gonna happen if value is :three? Easy, if a blanket else path is available, hard otherwise.

Some other languages have facilities to check that every possible value has a code path so that no unexpected implications have a chance to occur, but Crystal doesn’t.

We could use a different approach. Since Crystal doesn’t allow to overload methods on constants (enum State):

 8 | def state(s : State::One) : Nil
                   ^---------
Error: State::One is not a type, it's a constant

We’ll have to instead use honest types:

abstract struct State
  struct One < State; end
  struct Two < State; end
  struct Th3 < State; end
end

def state(s : State::One) : Nil; p one: s end
def state(s : State::Two) : Nil; p two: s end
def state(s : State::Th3) : Nil; p th3: s end

state (rand > 0.5) ? State::One.new : State::Two.new

Without state(State::Th3) the compiler would report an error:

 13 | state (rand > 0.5) ? State::One.new : State::Two.new
      ^----
Error: no overload matches 'state' with type State+

Overloads are:
 - state(s : State::One)
 - state(s : State::Two)
Couldn't find overloads for these types:
 - state(s : State::Th3)

This works because even though the temp variable should be State::One | State::Two, it’s actually a State+ because of the subtype folding optimisation.

This way you lose some of the enums sugar, which you can then manually add back with some macros and whatnot, but what you gain is built-in future proofing for adding new values.

Being able to use specific Enum members as a type restriction would be great. Iv’e came across a few cases where it would have been super helpful.

This also relates to https://github.com/crystal-lang/crystal/issues/8001

Yes, this will be eventually solved when we implement exhaustive case. But we’ll never implement overloading based on enum value. We never overload based on values, always on types, and enum members are values.

My intention wasn’t to push for a built-in language construct, I personally try to avoid case on types as overloading solves that more gracefully imo. Case is useful with regards to === methods as it can express complex logic switch quite elegantly there.

I simply want to show how that behaviour could be achieved right now with a slight modification of existing thought patterns.