rfc:tagged_unions

PHP RFC: Tagged Unions

  • Version: 1.0
  • Date: 2020-12-04
  • Author: Larry Garfield (larry@garfieldtech.com), Ilija Tovilo (tovilo.ilija@gmail.com)
  • Status: Draft

Introduction

This RFC expands on the previous Enumerations RFC to allow values to be associated with an enum Case, creating what is typically called a “tagged union.”

Many languages have support for enumerations of some variety. A survey we conducted of various languages found that they could be categorized into three general groups: Fancy Constants, Fancy Objects, and full Algebraic Data Types. This RFC expands the previous “Fancy Objects” implementation into full ADTs, also known as tagged unions. (The terminology varies by language.)

The specific implementation here draws inspiration primarily from Swift, Rust, and Kotlin, but is not (nor is it intended as) a perfect 1:1 port of any of them.

Proposal

Tagged Cases

Enumerated Cases may optionally include associated values. An associated value is one that is associated with an instance of a Case. If a Case has associated values, it will not be implemented as a singleton. Each instance of the Case will then be its own object instance, so will not === another instance.

A case with an associated value is called a Tagged Case. An Enum Case that does not have associated values is called a Unit Case.

Tagged cases are mutually exclusive with Scalar Cases. If an Enum has at least one Tagged Case, it may not have any Scalar Cases and vice versa. However, the same Enum may consist of any number of Tagged and Unit Cases.

Associated values are defined using constructor property promotion.

enum Distance {
    case Kilometers(public int $km);
    case Miles(public int $miles);
}
 
$my_walk = Distance::Miles(500);
// Named parameters work like any other function call.
$next_walk = Distance::Miles(miles: 500);
 
print $my_walk->miles; // prints "500"
 
$my_walk === $next_walk; // FALSE!

Tagged Cases may not implement a full constructor. However, they may list parameters that will be auto-promoted to properties using constructor promotion. The visibility modifier is required. Cases may not implement properties other than promoted properties.

The Enum Type itself may not define associated values. Only a Case may do so.

Associated values are always read-only, both internally to the class and externally. Therefore, making them public does not pose a risk of 3rd party code modifying them inadvertently. They may, however, have attributes associated with them like any other property.

On a Tagged Case enumeration, the cases() method is not available and will throw a TypeError. Since Tagged Cases are technically unbounded, the method has no logical sense.

Use cases that would require more complete class functionality (arbitrary properties, custom constructors, mutable properties, etc.) should be implemented using traditional classes instead.

Examples

Below are a few examples of Tagged Cases / Tagged Unions in action.

Maybe

The (in)famous Maybe Monad (also known as Option, Optional, or Result) can be implemented like this:

enum Maybe {
  // This is a Unit Case.
  case None {
    public function bind(callable $f) 
    {
      return $this;
    }
  };
 
  // This is a Tagged Case.
  case Some(private mixed $value) {
    // Note that the return type can be the Enum itself, thus restricting the return
    // value to one of the enumerated types.
    public function bind(callable $f): Maybe
    {
      // $f is supposed to return a Maybe itself.
      return $f($this->value);
    }
  };
 
  // This method is available on both None and Some.
  public function value(): mixed {
    if ($this instanceof None) {
      throw new Exception();
    }
    return $this->val;
  }
}

Limited Command List

To use an example inspired by the Rust documentation, the following defines a limited set of possible game commands. Some commands may have additional information that goes along with them. This approach ensures that all possible commands are listed together (it is a deliberately closed list), and no other commands are possible.

enum CardinalDirection {
  case North, South, East, West;
}
 
enum Direction {
  case Left, Right;
}
 
enum Command {
  case Move(public CardinalDirection $direction, int $distance);
  case Turn(public Direction $dir);
  case Shoot;
}

Backward Incompatible Changes

None, beyond what is already in the Enumerations RFC.

Proposed PHP Version(s)

PHP 8.1.

Open Issues

None.

Future Scope

Voting

This is a simple yes/no vote to include Tagged Unions. 2/3 required to pass.

References

rfc/tagged_unions.txt · Last modified: 2020/12/05 20:21 by crell