The typechecker considers this class requirement by the interface IBase
to be cyclic:
<?hh // strict
interface IBase {
require extends Derived;
}
class Derived implements IBase {}
// Cyclic class definition : IBase Derived (Typing[4013])
As I understand it, the constraint just prevents all descendants from implements IBase
without extends Derived
. Is there a hole with this that I'm not seeing?
Why do I care?
I'm interested in an interface that wants to compare against other instances of itself or its subtypes.
<?hh // strict
interface Comparable<-T as Comparable<T>> {
require extends ArtificialCeiling;
public function compare(T $comparee): bool;
}
abstract class ArtificialCeiling implements Comparable<ArtificialCeiling> {
abstract public function compare(ArtificialCeiling $comparee): bool;
}
(this
is not the answer here, because this
isn't sound in contravariant positions, especially in interfaces)
Suppose now we want to accept and store a wrapper of Comparable
but we don't care about what type of Comparable
it's lugging around. Normally, we'd just parameterize with the upper bound, or mixed
if its unconstrained.
The problem is that the upper bound for Comparable
is Comparable<Comparable<Comparable<...
forever, but I don't have the stamina to type that for all eternity. Without existential types like Scala or multiple constraints like TComparable as Comparable & ArtificialCeiling
, we have to resort to something less obvious. require extends ArtificialCeiling
would be just like a multiple constraint and, without this mysterious cyclic problem, it would be a tidy fix.
The other natural alternative is for the accepting class to append the parameter to its own parameter list as TComparable as Comparable<TComparable>
, but that defeats the principle of not caring about TComparable
.