case

A case is a control expression which functions a bit like pattern matching. It allows writing a chain of if-else-if with a small change in semantic and some more powerful constructs.

In its basic form, it allows matching a value against other values:

  1. case exp
  2. when value1, value2
  3. do_something
  4. when value3
  5. do_something_else
  6. else
  7. do_another_thing
  8. end
  9. # The above is the same as:
  10. tmp = exp
  11. if value1 === tmp || value2 === tmp
  12. do_something
  13. elsif value3 === tmp
  14. do_something_else
  15. else
  16. do_another_thing
  17. end

For comparing an expression against a case‘s value the case equality operator === is used. It is defined as a method on Object and can be overridden by subclasses to provide meaningful semantics in case statements. For example, Class defines case equality as when an object is an instance of that class, Regex as when the value matches the regular expression and Range as when the value is included in that range.

If a when‘s expression is a type, is_a? is used. Additionally, if the case expression is a variable or a variable assignment the type of the variable is restricted:

  1. case var
  2. when String
  3. # var : String
  4. do_something
  5. when Int32
  6. # var : Int32
  7. do_something_else
  8. else
  9. # here var is neither a String nor an Int32
  10. do_another_thing
  11. end
  12. # The above is the same as:
  13. if var.is_a?(String)
  14. do_something
  15. elsif var.is_a?(Int32)
  16. do_something_else
  17. else
  18. do_another_thing
  19. end

You can invoke a method on the case‘s expression in a when by using the implicit-object syntax:

  1. case num
  2. when .even?
  3. do_something
  4. when .odd?
  5. do_something_else
  6. end
  7. # The above is the same as:
  8. tmp = num
  9. if tmp.even?
  10. do_something
  11. elsif tmp.odd?
  12. do_something_else
  13. end

You may use then after the when condition to place the body on a single line.

  1. case exp
  2. when value1, value2 then do_something
  3. when value3 then do_something_else
  4. else do_another_thing
  5. end

Finally, you can omit the case‘s value:

  1. case
  2. when cond1, cond2
  3. do_something
  4. when cond3
  5. do_something_else
  6. end
  7. # The above is the same as:
  8. if cond1 || cond2
  9. do_something
  10. elsif cond3
  11. do_something_else
  12. end

This sometimes leads to code that is more natural to read.

Tuple literal

When a case expression is a tuple literal there are a few semantic differences if a when condition is also a tuple literal.

Tuple size must match

```{.crystal nocheck} case {value1, value2} when {0, 0} # OK, 2 elements

when {1, 2, 3} # Syntax error: wrong number of tuple elements (given 3, expected 2)

end

  1. ### Underscore allowed
  2. ```crystal
  3. case {value1, value2}
  4. when {0, _}
  5. # Matches if 0 === value1, no test done against value2
  6. when {_, 0}
  7. # Matches if 0 === value2, no test done against value1
  8. end

Implicit-object allowed

  1. case {value1, value2}
  2. when {.even?, .odd?}
  3. # Matches if value1.even? && value2.odd?
  4. end

Comparing against a type will perform an is_a? check

  1. case {value1, value2}
  2. when {String, Int32}
  3. # Matches if value1.is_a?(String) && value2.is_a?(Int32)
  4. # The type of value1 is known to be a String by the compiler,
  5. # and the type of value2 is known to be an Int32
  6. end

Exhaustive case

Using in instead of when produces an exhaustive case expression; in an exhaustive case, it is a compile-time error to omit any of the required in conditions. An exhaustive case cannot contain any when or else clauses.

The compiler supports the following in conditions:

Union type checks

If case‘s expression is a union value, each of the union types may be used as a condition:

  1. # var : (Bool | Char | String)?
  2. case var
  3. in String
  4. # var : String
  5. in Char
  6. # var : Char
  7. in Bool
  8. # var : Bool
  9. in nil # or Nil, but .nil? is not allowed
  10. # var : Nil
  11. end

Bool values

If case‘s expression is a Bool value, the true and false literals may be used as conditions:

  1. # var : Bool
  2. case var
  3. in true
  4. do_something
  5. in false
  6. do_something_else
  7. end

Enum values

If case‘s expression is a non-flags enum value, its members may be used as conditions, either as constant or predicate method.

  1. enum Foo
  2. X
  3. Y
  4. Z
  5. end
  6. # var : Foo
  7. case var
  8. in Foo::X
  9. # var == Foo::X
  10. in .y?
  11. # var == Foo::Y
  12. in .z? # :z is not allowed
  13. # var == Foo::Z
  14. end

Tuple literals

The conditions must exhaust all possible combinations of the case expression’s elements:

  1. # value1, value2 : Bool
  2. case {value1, value2}
  3. in {true, _}
  4. # value1 is true, value2 can be true or false
  5. do_something
  6. in {_, false}
  7. # here value1 is false, and value2 is also false
  8. do_something_else
  9. end
  10. # Error: case is not exhaustive.
  11. #
  12. # Missing cases:
  13. # - {false, true}