PHP 7, which will come out in November this year will introduce the Spaceship (<=>) operator. What is it and how does it work?
This question already has an answer in our general reference question about PHP operators.
PHP 7, which will come out in November this year will introduce the Spaceship (<=>) operator. What is it and how does it work?
This question already has an answer in our general reference question about PHP operators.
When reading the RFC we find out that it contradicts itself:
Add a new operator (expr) <=> (expr), it returns 0 if both operands are equal, 1 if the left is greater, and -1 if the right is greater. It uses exactly the same comparison rules as used by our existing comparison operators: <, <=, ==, >= and >. (See the manual for details)
Note: See the ==
, this means the spaceship operator does a loosely comparison.
And later down in the examples:
// only values are compared $a = (object) ["a" => "b"]; $b = (object) ["b" => "b"]; echo $a $b; // 0
The spaceship operator is just a combination of the operators <
, ==
and >
. And it gives respective return values depending on what it evaluates to:
operator(s): < = >
return value: -1 0 1
Now arrays and objects are a bit more complex types. To understand what the <=>
PHP spaceship operator does, we need to look and understand how <
, ==
and >
work for arrays and objects.
So let's look at the comparison operators <
, >
, ==
for each type. First we will look at <
and >
and then after that we also look at ==
.
Now as for arrays <
and >
are documented here:
???????????????????????????????????????????????????????????????????????????? ? type of ? type of ? ? ? Operand 1 ? Operand 2 ? Result ? ???????????????????????????????????????????????????????????????????????????? ? array ? array ? Array with fewer members is smaller, ? ? ? ? if key from operand 1 is not found in operand 2 ? ? ? ? then arrays are uncomparable, ? ? ? ? otherwise - compare value by value ? ????????????????????????????????????????????????????????????????????????????
This can also be written and represented by code:
Example #2 Transcription of standard array comparison
<?php // Arrays are compared like this with standard comparison operators function standard_array_compare($op1, $op2) { if (count($op1) < count($op2)) { return -1; // $op1 < $op2 } elseif (count($op1) > count($op2)) { return 1; // $op1 > $op2 } foreach ($op1 as $key => $val) { if (!array_key_exists($key, $op2)) { return null; // uncomparable } elseif ($val < $op2[$key]) { return -1; } elseif ($val > $op2[$key]) { return 1; } } return 0; // $op1 == $op2 } ?>
We can test this easily with some testing. Using methods like in math and always change only one thing, so we can make sure we are correct here:
/**
/*
/* Testing operators: < and >
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 9
// Failed: 0
// Passed: 9
{
//Test case 1.1
$a = [1];
$b = [1];
//Passed
var_dump("Same amount of elements, keys and values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.2
$a = [1];
$b = [1, 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.3
$a = [10];
$b = [1, 1];
//Passed
var_dump("NOT same amount of elements nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.4
$a = [1 => 1];
$b = [10 => 1];
//Passed
var_dump("Same amount of element and values, NOT same keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.5
$a = [10];
$b = [1];
//Passed
var_dump("Same amount of elements and keys, NOT same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.6
$a = [1 => 1, 2 => 1];
$b = [2 => 1, 1 => 1];
//Passed
var_dump("Same amount of elements and keys in different order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.7
$a = [1 => 1, 2 => 5];
$b = [2 => 5];
//Passed
var_dump("Same values, NOT same amount of elements nor keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.8
$a = [10 => 1];
$b = [1 => 10];
//Passed
var_dump("NOT same keys nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.9
$a = [1 => 1, 2 => 1];
$b = [2 => 10, 1 => 1];
//Passed
var_dump("Same amount of elements and values, NOT same keys nor order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
For the equality/identity operators ==
and ===
we find the documentation for arrays here:
??????????????????????????????????????????????????????????????????????????? ? Example ? Name ? Result ? ??????????????????????????????????????????????????????????????????????????? ? $a == $b ? Equality ? TRUE if $a and $b have the same key/value pairs. ? ? $a === $b ? Identity ? TRUE if $a and $b have the same key/value pairs ? ? ? ? in the same order and of the same types. ? ???????????????????????????????????????????????????????????????????????????
As before we can simply test this with some testing code:
/**
/*
/* Testing operators: == and ===
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 5
// Failed: 0
// Passed: 5
{
//Test case 2.1
$a = [1];
$b = [1];
//Passed
var_dump("Same amount of elements, values and keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.2
$a = [1];
$b = [10, 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.3
$a = [10];
$b = [1];
//Passed
var_dump("Same amount of elements, but not values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.4
$a = [1 => 1];
$b = [10 => 1];
//Passed
var_dump("Same amount of elements and values, but not keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.5
$a = [1 => 1, 2 => 2];
$b = [2 => 2, 1 => 1];
//Passed
var_dump("Same amount of elements, key and values, but different order: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
So we can see and confirm that the comparison operators for arrays work as expected and as documented!
Full Testing File
The documentation for <
and >
with objects is documented here:
???????????????????????????????????????????????????????????????????????????? ? type of ? type of ? ? ? Operand 1 ? Operand 2 ? Result ? ???????????????????????????????????????????????????????????????????????????? ? object ? object ? Built-in classes can define its own comparison, ? ? ? ? different classes are uncomparable, ? ? ? ? same class compare properties same as arrays ? ????????????????????????????????????????????????????????????????????????????
As before we can also test this:
/**
/*
/* Testing operators: < and >
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 10
// Failed: 0
// Passed: 10
{
//Test case 1.1
$a = (object)["a" => 1];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements, keys and values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.2
$a = (object)["a" => 1];
$b = (object)["a" => 1, "b" => 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.3
$a = (object)["a" => 10];
$b = (object)["a" => 1, "b" => 1];
//Passed
var_dump("NOT same amount of elements nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.4
$a = (object)["a" => 1];
$b = (object)["b" => 1];
//Passed
var_dump("Same amount of element and values, NOT same keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.5
$a = (object)["a" => 10];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements and keys, NOT same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.6
$a = (object)["a" => 1, "b" => 1];
$b = (object)["b" => 1, "a" => 1];
//Passed
var_dump("Same amount of elements and keys in different order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.7
$a = (object)["a" => 1, "b" => 5];
$b = (object)["b" => 5];
//Passed
var_dump("Same values, NOT same amount of elements nor keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.8
$a = (object)["c" => 1];
$b = (object)["a" => 10];
//Passed
var_dump("NOT same keys nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.9
$a = (object)["a" => 1, "b" => 1];
$b = (object)["b" => 10, "a" => 1];
//Passed
var_dump("Same amount of elements and values, NOT same keys nor order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.10
class A {public $a = 1;}
$a = new A;
class B {public $a = 1;}
$b = new B;
//Passed
var_dump("Same amount of elements and values and keys, but different not built-in class: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
The documentation for ==
and ===
with objects has its own page here:
When using the comparison operator (==), object variables are compared in a simple manner, namely: Two object instances are equal if they have the same attributes and values, and are instances of the same class.
When using the identity operator (===), object variables are identical if and only if they refer to the same instance of the same class.
And again this can be tested:
/**
/*
/* Testing operators: == and ===
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 7
// Failed: 0
// Passed: 7
{
//Test case 2.1
$a = (object)["a" => 1];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements, values and keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.2
$a = (object)["a" => 1];
$b = (object)["a" => 10, "b" => 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.3
$a = (object)["a" => 10];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements, but not values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.4
$a = (object)["a" => 1];
$b = (object)["b" => 1];
//Passed
var_dump("Same amount of elements and values, but not keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.5
$a = (object)["a" => 1, "b" => 2];
$b = (object)["b" => 2, "a" => 1];
//Passed
var_dump("Same amount of elements, key and values, but different order: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.6
class C {public $a = 1;}
$a = new A;
class D {public $a = 1;}
$b = new B;
//Passed
var_dump("Same amount of elements and values and keys, but different not built-in class: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.7
$a = (object)["a" => 1];
$b = $a;
//Passed
var_dump("Same exact instance: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
So we see, that the comparison operators with objects behave exactly like expected and documented! Even with loose comparison the attributes and values are being considered.
Full Testing File
As this bug has been reported here, the bug report is probably based on the comment in the RFC, which says:
// only values are compared
But besides that this is the only example in the RFC with a comment, the RFC clearly states that it uses the same comparison rules as <
, ==
and >
.
This means that the code example provided would be uncomparable, because it doesn't have the same attributes/keys.
As for equality it would need same attributes/keys and values so it can't be equal, and for less- or greater- than it is uncomparable as shown in the code example above how the comparison works:
if (!array_key_exists($key, $op2)) {
return null; // uncomparable
}
We also see this if we try each comparison operator alone:
$a = (object)["b" => "b"];
$b = (object)["a" => "b"];
var_dump($a > $b); //FALSE
var_dump($a < $b); //FALSE
var_dump($a == $b); //FALSE
All return false, since it's uncomparable.
And just for the case StdClass
would have its own comparison, we can test it with our own class:
class A {
public $a = "";
public $b = "";
}
$a = new A;
$a->a = "b";
unset($a->b);
$b = new A;
$b->b = "b";
unset($b->a);
var_dump($a);
var_dump($b);
var_dump($a <=> $b); //1
Also same output: 1.
So I would say since it is uncomparable it shouldn't return 0
, 1
nor -1
. It should probably return FALSE
or NULL
or something like this.
Right now I would say this behaviour isn't documented correctly.
I'll give you three points of motivation, just off the top of my head:
>
, >=
, ==
, <=
, <
. Using <=>
(spaceship), you can implement each of these other operations in a completely generic way.strcmp()
function from the C standard library. So - useful for lexicographic order checks, such as data in vectors or lists or other ordered containers.x86
or x86_64
Comparing a and b (CMP RAX, RBX
) is basically like subtracting (SUB RAX, RBX
) except that RAX
doesn't actually change, only the flags are affected, so you can use "jump on equal/not equal/greater than/lesser than/etc." (JE/JNE/JGT/JLT etc.) as the next instruction. CMP
should be thought of as a "spaceship compare".Would define
operator>(a,b)
asa<=>b > 0
not lead to large overhead?
It would lead to some overhead. The magnitude of the overhead is relative, though - in situations when costs of running comparisons are negligible in relation to the rest of the program, reducing code duplication by implementing one operator instead of five may be an acceptable trade-off.
However, the proposal does not suggest removing other comparison operators in favor of <=>
: if you want to overload other comparison operators, you are free to do it:
Be general: Don’t restrict what is inherent. Don’t arbitrarily restrict a complete set of uses. Avoid special cases and partial features. – For example, this paper supports all seven comparison operators and operations, including adding three-way comparison via
<=>
. It also supports all five major comparison categories, including partial orders.
I'm writing a control system for robot Joe that wants to go to robot Mary and recharge her. They move along the integer points on the line. Joe starts at $j and can walk 1 meter in any direction per time unit. Mary stands still at $m and can't move -- she needs a good recharge! The controlling program would look like that:
while ($m != $j) {
$j += ($m <=> $j);
}
The
<=>
("Spaceship") operator will offer combined comparison in that it will :The rules used by the combined comparison operator are the same as the currently used comparison operators by PHP viz.
<
,<=
,==
,>=
and>
. Those who are from Perl or Ruby programming background may already be familiar with this new operator proposed for PHP7.