Type matching in PHP
One of the nice features of Rust is the match
keyword. match
is similar to switch
, but with two key differences:
- It requires an exhaustive match, that is, every possible value must be accounted for or a default must be provided.
match
is an expression, meaning you can assign the return value of one of its branches to a variable.
That makes match
extremely useful for ensuring you handle all possibilities of an enumerated type, say, if using an Optional or Either
for error handling. Which... is something I've been experimenting with in PHP.
It's hard to make a PHP equivalent of match
that forces an exhaustive match, as PHP lacks enumerated types. However, emulating an expression match turns out to be pretty easy in PHP 7.4, and kind of pretty, too.
The concept
Sometimes we are dealing with a variable of unclear type, but the possible types it could be is a closed set. That could be
- a
Maybe
orEither
style monad, - a pseudo-enumerated type (which is the fancy way of saying a limited number of classes that implement a certain interface),
- a union return value in PHP 8
or various other cases. In that situation we want to cleanly branch our logic based on the type, at least somewhat, but polymorphic inheritance on the object isn't sufficient; the thing we want to branch doesn't make logical sense as a method of the object.
The naive way
So we have a $var
that we know is of type A|B|C
, but not which of those types it is. We could just check it manually:
if ($var instance of A) {
$result = ...;
} else if ($var instance of B) {
$result = ...;
} else if ($var instance of C) {
$result = ...;
} else {
$result = 'Some default';
}
But that's kind of ugly and doesn't really express visually that we're doing a type-based branch. It also doesn't do a good job of visualizing that "set $result
" is the end goal. Any of those if
blocks could technically do anything and not set $result, in which case we can't guarantee that it's now set without examining each branch.
We can do better.
Short-lambdas to the rescue
PHP 7.4 introduced "short lambdas", which are a short-hand way to write single-statement anonymous functions that don't even look much like functions. They look more like normal expressions. That's the point. That allows us to refactor the above block into something like this:
$result = match($var, [
A::class => fn() => 'A',
B::class => fn() => 'B',
C::class => fn() => 'C',
], fn() => 'Default');
That is, we provide a literal map from class type to a single statement to run, wrapped into a closure. That closures, because it's a short lambda, can auto-capture variables from its scope, including $var
itself. Because it's a function, none of them run at this point; they're just values, like any other value, that do nothing until invoked. So, let's invoke them:
function match($var, array $map, callable $default = null) {
foreach ($map as $type => $fn) {
if ($var instanceof $type) {
return $fn();
}
}
if ($default) {
return $default();
}
throw new TypeError();
}
That is, scan through the map of callables until we find one that type matches, then call that and return the result. If none match, we also support a default fallback. If there is no fallback provided, well, the developer screwed up so throw a TypeError
. (Error indicating "the dev screwed up and the code is wrong", not "some oddball exceptional case happened", for which you'd likely throw InvalidArgumentException
or similar.)
This approach guarantees that $result
is now set, and makes that clear visually; if it's not, it's a developer error and an appropriate error is thrown.
An alternate implementation would be to pass $var
as an argument to each lambda, but since it's auto-captured anyway if necessary that would in practice just mean more typing.
It's not as nice as Rust's match
keyword, but once you start using PHP for functional programming (yep, that's a thing) I expect it will come in quite handy.
OMG Arrays?
Yes, it's an actual use case for a literal associative array in PHP, in which an object would not be superior.
I'm scared, too.