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
- First Published at: http://wiki.php.net/rfc/tagged_unions
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.
MongoDB Driver
The MongoDB team has reached out to us with another example of what they would like to be able to do in their driver.
Their planned code is below, for reference. For more details and an explanation of their intent, see this gist.
enum ReadPreference { case Primary; case Secondary(?array $tagSets = null, ?int $maxStalenessSeconds = null); case PrimaryPreferred(?array $tagSets = null, ?int $maxStalenessSeconds = null); case SecondaryPreferred(?array $tagSets = null, ?int $maxStalenessSeconds = null); case Nearest(?array $tagSets = null, ?int $maxStalenessSeconds = null); static function withTagSets(?array $tagSets = null): self {} static function withMaxStaleness(?int $maxStalenessSeconds = null): self {} } enum ReadConcern { case Available; case Linearizable; case Local; case Majority; case Snapshot(?Timestamp $atClusterTime = null); } enum WriteConcern { case Majority(?int $timeout = null); case Unacknowledged; case Acknowledged(int $servers = 1, ?int $timeout = null); static function withTimeout(?int $timeout = null): self {} }
Backward Incompatible Changes
None, beyond what is already in the Enumerations RFC.
Proposed PHP Version(s)
PHP 8.3.
Open Issues
None.
Future Scope
See the Algebraic data types (Meta RFC) document.
Voting
This is a simple yes/no vote to include Tagged Unions. 2/3 required to pass.