Updated
The more complete answer is allowing subclasses to inherit a this
-argumented method is fundamentally flawed. Starting with the basics, function arguments are contravariant, so we can cast them downward because the function can accept any subtypes of the type it specifies, but it can't accept supertypes. So, the cast function(Derived): void
to function(Base): void
for Derived <: Base
is invalid. By extension, the following hierarchy is invalid because Derived <: Base
, but the cast of Derived
to Base
casts foo(...)
in the same way:
<?hh // strict
interface Base {
public function foo(Base $v): void;
}
interface Derived extends Base {
<<__Override>>
public function foo(Derived $v): void;
}
// Derived -> Base means foo(Derived): void -> foo(Base): void
Now, note that the this
type is doing exactly that, even if Derived
doesn't explicitly override foo(...)
. In other words, the following is exactly equivalent:
<?hh // strict
interface Base {
public function foo(this $v): void;
}
interface Derived extends Base {}
The simplest violation I can think of:
<?hh
interface Base {
public function foo(this $v): void;
}
final class Derived implements Base {
public function bar(): void {}
public function foo(this $v): void {
$v->bar(); // trying to call `bar()` on OtherDerived fails miserably!
}
}
final class OtherDerived implements Base {
public function foo(this $v): void {}
}
function violate(Base $v, Base $x): void {
$v->foo($x);
}
bar(new Derived(), new OtherDerived());
However, if we can be sure we have a specific descendant of Base
, then we know exactly what its foo(...)
wants: its own type, and only its own type. We can apply this in general for the overriding shown in the first code block. This isn't very useful in general, but I assume this
is so nice and concise that it was worth allowing to write the interfaces, then checking every invocation.
Original
Without the error, the following violation would be possible:
<?hh // strict
class A {
public function act(this $v): void {
$v->act($this);
}
}
class B extends A {
public ?int $b_prop;
<<__Override>>
public function act(this $v): void {
$v->b_prop;
}
}
function initiate(): void {
violate(new B());
}
function violate(A $v): void {
(new A())->act($v);
}
I don't know if this is the archetypical violation for this error, but my guess at the rationale:
A::act(this)
is public/protected, so by its existence, there exists at least one method with a this
argument available to subclasses.
- The body of
act
can call at least $v->act
which has a this
argument.
act
can be called from an A
context, so the argument this
is exactly A
. It follows that this
in $v
's context is also cast to A
.
- Because
A
is not final, it's possible that $v
is a proper subtype of A
.
- The underlying class of
$v
(in this case, B
) can override any this
-argumented methods from A
under the assumption that this <: B
. In this case, $v->b_prop
relies on that assumption.
- The
A
context thinks it can pass an A
type (e.g. itself) to the B
by (3.), but that violates the assumption in (5.)