kphp icon indicating copy to clipboard operation
kphp copied to clipboard

Exhaustive `switch` operator

Open vkaverin opened this issue 3 years ago • 1 comments

Problem

class Var {
  public const A = 0x1;
  public const B = 0x2;
}

...

switch ($var) {
  case Var::A:
    return a_work();  
  case Var::B:
    return b_work();
}

Here we have a class that is basically an enum (though PHP doesn't have one). it has two variants, A and B. Also there's a switch that is supposed to cover all possible variants of Var. Now you want to add new variant C = 0x3, but you forget to add it to the switch by some reason - now you have a bug with uncovered variant.

Solution

Inspired by Rust's match operator.

Introduce new annotation @kphp-match Type $v like following:

/** @kphp-match Var $var */
switch ($var) {
  ...
}

Now the compiler will check whether all of the possible are covered by the switch and fail compilation if they are not.

Of course there are cases when only a subset of variant is need to be processed. In this case we can still use default to cover them:

class Var {
  public const A = 0x1;
  public const B = 0x2;
  public const C = 0x3;
  public const D = 0x4;
}

...

/** @kphp-match Var $var */
switch ($var) {
  case Var::A:
    return a_work();  
  case Var::B:
    return b_work();
  default:
    // We do not care about anything except but A and B.
    // Or can raise a warning, if we want to.
}

More use cases

Switch by class

Except const as enum use case, another possible case is coverage of all variants for switch (get_class($o)), because if $o has type T, compiler knows all possible subtypes (child classes) or implementation classes (if T is an interface`):

/** @kphp-match-type $o */ // Or use @kphp-match here too, depending on implementation details.
switch (get_class($o)) {
  case A::class:
    // $o can be smart-casted to A here.
    return a_work();  
  case B::class:
    return b_work();
}

Const map keys

Sometimes it may be necessary to require a map to have all possible keys:

class Mappings {
  
  /** @kphp-match Var */
  private const MAPPING = [
    Var::A => 'a',
    Var::B => 'b',
    Var::C => 'c',
    // Compile time error if there's no Var::D
  ];

  public static map(int $v): ?string {
    return self::MAPPING[$v] ?? null;
  }
}

Other languages support

Languages that support similar feature:

  • Rust
  • Kotlin
  • Java
  • Go (only linter support yet)
  • Scala (match on sealed trait)

vkaverin avatar Feb 07 '22 23:02 vkaverin

Will be partially implemented in PHP 8's match operator (#303), but it doesn't cover cases with exhaustion over class constants.

vkaverin avatar Feb 14 '22 20:02 vkaverin