0

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.

concat
  • 3,107
  • 16
  • 30

1 Answers1

1

Well, I'm not an expert, but the message seems clear to me: the definition is cyclic, because Derived uses IBase and IBase references Derived. According to the documentation:

require extends should be taken literally. The class must extend the required class; thus the actual required class does not meet that requirement. This is to avoid some subtle circular dependencies when checking requirements.

I think the way to go is to specify the requirement for an ancestor class, and implement the interface in the non-abstract derived class(es). Or maybe just implement compare as a normal method in the ancestor class, instead of using a trait.

szmate1618
  • 1,545
  • 1
  • 17
  • 21