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.

If two Tagged Case instances have the same associated value, the object instance will be reused. That means two instances that have the same associated values will pass both a == check and a === check.

enum Distance {
    case Kilometers(int $km);
    case Miles(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; // TRUE!
$my_walk === $next_walk; // TRUE!

Tagged Cases may not implement a full constructor. The associated values specified will be available as public readonly properties on the case object. Associated values may have property-targeting attributes associated with them.

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

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(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);
    }
  };
}

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(CardinalDirection $direction, int $distance);
  case Turn(Direction $dir);
  case Shoot;
}

Static analysis type tracking

PHP's types could be represented with an enum like so:

enum Type
{
  case Int;
  case Float;
  case String;
  case Array;
  case Bool;
  case Object(string $class);
}

(I really have had to do something like that before.) Combined with pattern matching, this would allow for a very robust way to handle objects being special and having additional context.

Backward Incompatible Changes

None, beyond what is already in the Enumerations RFC.

Proposed PHP Version(s)

PHP 8.3.

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: 2023/01/18 18:40 by crell