Open acrylic-origami opened 8 years ago
Hey @dlreeves this is interesting -- can you make sure Andrew Kennedy sees this, seems like this sort of thing he's been looking into recently. And maybe also tell him to sign up for GitHub and put the email address he commits with on the account and use our internal tool to add him as a contributor to HHVM so I can cc him properly :-P
I've stared at this a bit longer, and I think allowing +T
-typed arguments in constructors is itself problematic, because functionality in B::__construct
could still assume that/those argument[s] are constrained by Derived
. Then new $v(new Base())
in C::foo
alone would violate that assumption.
From my understanding, usually the combined fact of the constructor only being executed exactly once at instantiation, and the calling scope knowing exactly what class is being instantiated, implies that the constructor can't modify type. classname<T>
allowing a cast before instantiation strips the constructor of its sacred first-type-related-thing-to-execute property, which lets the constructor mishandle types.
Another good catch! Even without classname, there is a problem with the semantics of "private" (meaning accessible from the code of the class but at any instantiation). Consider the code below, which breaks the type system. (Scala has a notion of "object-private" to avoid the typehole. See http://www.scala-lang.org/files/archive/spec/2.11/04-basic-declarations-and-definitions.html#variance-annotations. I'm considering doing something similar in Hack.)
<?hh // strict
class Box<+T> {
// OK, we've got a private field whose type involves the covariant T
public function __construct(private T $elem) {
}
// As usual, a (safe) getter method
public function get(): T { return $this->elem; }
// Private gives us access to arbitrary instances of Box, even in static
// methods. Note the use of covariant subtyping to put a string in
// a Box<mixed>
public static function updateAsString(Box<mixed> $x, string $s) : void {
$x->elem = $s;
}
// We can now use this method to overwrite an integer with a string
// but return it as an integer
public static function morphIntToString(int $i) : int {
$x = new Box($i);
Box::updateAsString($x, 'hey you turned me into a string');
return $x->get();
}
// Actually do it
public static function useBox(): void {
$i = Box::morphIntToString(23);
echo('this should be an integer: ' . $i);
}
}
The issue with object-private above is orthogonal.
Your analysis is good. One small note: B itself does not have to be covariant. It's enough to have a constraint on the parameter and covariance in the superclass.
What if __ConsistentConstruct
required the constructor signatures and constraints on type parameters occurring in the signatures to be preserved down the hierarchy? That way we wouldn't be able to even write A<Base>
.
I think what you suggest with __ConsistentConstruct
will work completely! Here's my thoughts on some of the weirder cases that it still covers:
The absence of static classes in Hack disallows the constructor to violate type with something like:
public function __construct(T $v) {
StaticClass<T>::v = $v;
}
To borrow C#'s behaviour in this case: if this were possible, because generic methods retain their original parameterization, the new $v(new Base());
call in C::bar(classname<A<Base>> $v)
actually modifies StaticClass<Derived>
. Not a problem in Hack!
Derived
and T
, so the other methods of manufacturing types, like type constants and generics via angle bracket syntax, pose no problems.C::foo
to act on members associated with the Base
type, simply because there are no concrete objects to act upon. The static members of whatever classname<B<Derived>> $v
is cannot be typed with generic types.Still, in C::bar
, $v
's constructor is really a (function(Derived): void)
. It being passed a Base
sends chills down my spine, but if provably no implementation couldn't be implemented as a (function(Base): void)
, I suppose that's good enough? I'm curious if there any implementation difficulties stemming from the invalidity in the general case.
HHVM Version
Standalone code, or other way to reproduce the problem
Define a class covariant on one of its type parameters and assert the constructor is consistent:
Then extend that class and constrain that same type parameter to a derived type, and act on that type in some methods:
Cast the
classname
of the extended class to theclassname
of the base class parameterized with a supertype of the extended class's constraint. Invoke a method that acts on the type parameter in the extended class.This is where B needs to be covariant on
T
: the typechecker doesn't allow this cast if it isn't.Expected result
Some sort of variance violation? I don't know which is the more illegal step: allowing private properties of a covariant type, or the classname cast.
Actual result
No errors!
from the typechecker, butfrom executing
C::foo()
.