whateverthing.com

PHP Tips: SPACESHIP!!!

Sorting. It's a core part of computer science. You use it every day, intentionally or not. You might even have whiteboard nightmares about it.

PHP 7.0 added a new operator designed specifically to make sorting a little bit easier. It's called the Spaceship Operator, and it looks a bit odd: <=>.

<=> Ubiquitous

When sorting information, computers need to be able to compare two pieces of data and determine whether A is less than B, equal to B, or greater than B.

PHP provides several user-defined sorting helpers which accept custom anonymous or callable sorting functions. The sorting function you pass in as the parameter to these helpers needs to return -1, 0, or 1 - that's it. From that information, PHP can handle the rest of the sorting operation.

<=> Mendacious

You probably recognize that having three possible outputs for a return value puts you in a sticky situation. How do you concisely determine which of the three outcomes you want to return?

Usually you'll end up with code that looks something like this:

$sortFunction = function ($a, $b) {
    if ($a > $b) {
        return 1;
    }

    if ($a < $b) {
        return -1;
    }

    return 0;
}

In an older codebase, it might follow this pattern (but the above approach is generally easier to read and maintain):

$sortFunction = function ($a, $b) {
    $sort = null;
    if ($a > $b) {
        $sort = 1;
    } else if ($a < $b) {
        $sort = -1;
    } else {
        $sort = 0;
    }

    return $sort;
}

If the interior comparison is more complex, then this results in even more lines of code necessary to express what seems "simple" on the surface:

$ships = [];
$ships[] = ['shipName' => 'Weeping Somnambulist', 'shipRank' => 2, 'crew' => 4];
$ships[] = ['shipName' => 'Rocinante', 'shipRank' => 1, 'crew' => 4];
$ships[] = ['shipName' => 'Razorback', 'shipRank' => 2, 'crew' => 2];

$sortFunction = function ($a, $b) {
    $sort = null;
    if ($a['shipRank'] > $b['shipRank']) {
        $sort = 1;
    } else if ($a['shipRank'] < $b['shipRank']) {
        $sort = -1;
    } else {
        if ($a['crew'] > $b['crew']) {
            $sort = 1;
        } else if ($a['crew'] < $b['crew']) {
            $sort = -1;
        } else {
            $sort = 0;
        }
    }

    return $sort;
};

usort($ships, $sortFunction);

print_r(array_column($ships, 'shipName'));

// Output:
// Array
// (
//     [0] => Rocinante
//     [1] => Razorback
//     [2] => Weeping Somnambulist
// )

That tangled mess is what becomes necessary to describe "sort by shipRank, ascending, then by crew, ascending".

It ain't a pretty sight.

<=> Polyglottal

Spaceship Operator to the rescue!

Instead of handling the logic using complex if statements - or, if you've really foot-gunned yourself, nested ternary statements - you can use the spaceship to shake free.

The spaceship operator (<=>) is just like a regular comparison operator, except instead of returning true or false, it can directly return the sorting-friendly values of -1, 0, and 1.

If you couple that with array comparison logic, you can cut through the noise of the if-elseif-if-elseif-else:

$ships = [];
$ships[] = ['shipName' => 'Weeping Somnambulist', 'shipRank' => 2, 'crew' => 4];
$ships[] = ['shipName' => 'Rocinante', 'shipRank' => 1, 'crew' => 4];
$ships[] = ['shipName' => 'Razorback', 'shipRank' => 2, 'crew' => 2];

$sortFunction = function ($a, $b) {
    return [$a['shipRank'], $a['crew']] <=> [$b['shipRank'], $b['crew']];
};

usort($ships, $sortFunction);

print_r(array_column($ships, 'shipName'));

// Output:
// Array
// (
//     [0] => Rocinante
//     [1] => Razorback
//     [2] => Weeping Somnambulist
// )

And, if you go back to the previous post and pull in some Arrow Function goodness, you can get even more svelte:

$ships = [];
$ships[] = ['shipName' => 'Weeping Somnambulist', 'shipRank' => 2, 'crew' => 4];
$ships[] = ['shipName' => 'Rocinante', 'shipRank' => 1, 'crew' => 4];
$ships[] = ['shipName' => 'Razorback', 'shipRank' => 2, 'crew' => 2];

usort(
    $ships,
    fn($a, $b) => [$a['shipRank'], $a['crew']] <=> [$b['shipRank'], $b['crew']]
);

print_r(array_column($ships, 'shipName'));

// Output:
// Array
// (
//     [0] => Rocinante
//     [1] => Razorback
//     [2] => Weeping Somnambulist
// )

Is that easier to read? Maybe. In my opinion, keeping everything tight means that you don't have to worry about missing logic that has scrolled out of view. It should make code review go more smoothly, as well.

<=> Donkey Balls

For more information on the origin of PHP's spaceship operator, the Combined Comparison Operator RFC document is available on the PHP wiki.

This concludes the first cycle of PHP Tips. I've covered Null Coalesce, Short Arrow Functions, and the Spaceship Operator. Stay tuned to hear about more recent PHP goodness, like Generators, Typed Properties, Unpacking Arrays, OPcache Preloading, FFI, Custom Serialization, not to mention all of the sweet new features coming in PHP 8. Reach out on Twitter or in the comments below if you want me to shed light on other features not listed here.

Published: May 5, 2020

Categories: coding

Tags: dev, development, coding, php, php-tips