Comparing apples with oranges makes no sense in pretty much no situation. An error in such a situation is the most sensible thing to do. How should PHP behave otherwise in such a situation? Suppose we want to sort an array of apples and oranges and usort gets null, what is it supposed to do? The resulting array will have arbitrary sorting and the developer will never know unless she tests the s*** out of it. This is a suboptimal situation and PHP should help the developer right away.
This would be possible if the Comparable methods would restrict their arguments to themselves (e.g. static which is not possible). Leaving these checks to the implementor might seem like a good idea but in the end just cries out loud for bugs. Especially considering that the runtime is capable of performing those checks including the correct error and a nice message.
It is possible to code this in PHP 5 and 7 already but with a few restrictions and of course without native support in existing PHP functions. However, it provides all desired safety features. I am confident that a few changes to the runtime could make this super useful.
The following is inspired by Haskell, Rust, Ceylon, and Hack.
<?php interface Equalable { function isEqualTo($other): bool; } /** * @method Ordering comparedTo(self $other) * @method static int compare(self $a, self $b) * @method static int compareReversed(self $a, self $b) * @method bool isLessThan(self $other) * @method bool isLessThanOrEqualTo(self $other) * @method bool isGreaterThan(self $other) * @method bool isGreaterThanOrEqualTo(self $other) */ interface Comparable extends Equalable { static function getCompareClosure(): Closure; static function getCompareReversedClosure(): Closure; } /** * @mixin Comparable */ trait Comparison /*requires Comparable*/ { abstract public function comparedTo(self $other): Ordering; final public static function compare(self $a, self $b): int { return $a->comparedTo($b)->getOrdinal(); } final public static function getCompareClosure(): Closure { return Closure::fromCallable([static::class, 'compare']); } final public static function compareReversed(self $a, self $b): int { return $a->comparedTo($b)->getReverse()->getOrdinal(); } final public static function getCompareReversedClosure(): Closure { return Closure::fromCallable([static::class, 'compareReversed']); } final public function isLessThan(self $other): bool { return $this->comparedTo($other)->isLess(); } final public function isLessThanOrEqualTo(self $other): bool { return $this->comparedTo($other)->isLessOrEqual(); } public function isEqualTo($other): bool { return $other instanceof $this && $this->comparedTo($other)->isEqual(); } final public function isGreaterThan(self $other): bool { return $this->comparedTo($other)->isGreater(); } final public function isGreaterThanOrEqualTo(self $other): bool { return $this->comparedTo($other)->isGreaterOrEqual(); } } /*enum*/ final class Ordering implements Comparable { use Comparison; public const LT = -1; public const EQ = 0; public const GT = 1; private static $less; private static $equal; private static $greater; private $ordinal; private function __construct(int $ordinal) { assert(self::LT <= $ordinal && $ordinal <= self::GT); $this->ordinal = $ordinal; } public static function createFromOrdinal(int $ordinal): Ordering { if ($ordinal < self::EQ) { return self::LESS(); } if ($ordinal > self::EQ) { return self::GREATER(); } return self::EQUAL(); } public static function LESS(): Ordering { return self::$less ?? self::$less = new static(self::LT); } public static function EQUAL(): Ordering { return self::$equal ?? self::$equal = new static(self::EQ); } public static function GREATER(): Ordering { return self::$greater ?? self::$greater = new static(self::GT); } public function comparedTo(self $other): Ordering { return self::createFromOrdinal($this->ordinal <=> $other->ordinal); } public function getOrdinal(): int { return $this->ordinal; } public function getReverse(): Ordering { return self::createFromOrdinal(-$this->ordinal); } public function isLess(): bool { return $this->ordinal === self::LT; } public function isLessOrEqual(): bool { return $this->isLess() || $this->isEqual(); } public function isEqual(): bool { return $this->ordinal === self::EQ; } public function isGreater(): bool { return $this->ordinal === self::GT; } public function isGreaterOrEqual(): bool { return $this->isGreater() || $this->isEqual(); } } $data = [Ordering::GREATER(), Ordering::LESS(), Ordering::EQUAL()]; usort($data, Ordering::getCompareClosure()); var_dump($data); usort($data, Ordering::getCompareReversedClosure()); var_dump($data); [$gt, $eq, $lt] = $data; var_dump( $lt->isLessThan($lt), $lt->isLessThanOrEqualTo($lt), $lt->isEqualTo($lt), $lt->isGreaterThan($lt), $lt->isGreaterThanOrEqualTo($lt), $lt->isEqualTo('garbage') ); // TypeError :) var_dump($lt->comparedTo('garbage'));
@Netmosfera was completely right that this is not the case for Equalable where we always want a bool result regardless what we pass in and just because we have Equalable does not mean that we have Comparable support. These things are different and the result of $comparable->isEqualTo() might differ from $comparable->comparedTo($other)->isEqual() which is perfectly fine and sound. There are many controversial discussions regarding this in regards to many standards, a few examples off the top of my head:
- Decimal values, e.g.
0.0and0.00are not equal but have the ordering 0. - Date and time with different offsets, e.g.
2000-00-00T00:00+01:00and2000-00-00T01:00+02:00are not equal but have the ordering 0.
You might disagree and say that they are equal, that's fine, people have different opinions on these. Those examples are meant to illustrate that Equlable and Comparable must not be consistent.
PS: Rust has different traits (interfaces) for different scenarios to communicate better what kind of ordering and equality certain types have: https://doc.rust-lang.org/std/cmp/