Skip to content

Commit 2ff573c

Browse files
Michael Thomasfacebook-github-bot
authored andcommitted
Polymorphic static method refs
Summary: Updates typing of function references to polymorphic static methods so that we obtain a polymorphic function type. In particular the typing deals with the following: - Use of method-level generics - Use of class-level generics - `where` constraints - Use of `this` - Use of abstract type constants ## Method-level generics The current type of function references will not return a polymorphic function type. Instead, when referencing a polymorphic method the typing introduces type variables and adds bounds corresponding to any constraints on the generic it replaces. Given the declaration of `bar`: ```[hack] class Wibble<Tclasslevel as arraykey> { public static function bar<Tmethodlevel as arraykey>( Tmethodlevel $y, ): void {} } function refEm(): void { $fptr = Wibble::bar<>; } ``` a fresh type variable will be created with an upper bound of `arraykey` and `$fptr` will be given the type ``` readonly function(#1 $x): #1 ``` where the type variable `#1` has an upper-bound of `arraykey`. Using `$fptr` will be introduce additional constraints on the type variables so the following is currently ill-typed: ```[hack] function expect_int_int((function(int): int) $f): void {} function refEm(): (function(string): string) { $fptr = Wibble::bar<>; expect_int_int($fptr); return $fptr; } ``` After passing `$fptr` to `expect_int_int` the type variable will be solved to `int` so at the return expression we encounter ``` (function(int): int </: function(string): string) ``` With the new typing contained in this diff the function reference will instead be given a polymorphic function type: ``` HH\FunctionRef<(readonly function<Tmethodlevel as arraykey>(Tmethodlevel): Tmethodlevel)> ``` Subtyping of polymorphic function types permits us to pass this function where an expression of type `(function(int): int)` is expected. Doing so does not change the type and we are free to return `$fptr` where an expression of type `(function(string): string)` is expected. Note that this typing is consistent with the already landed typing of generic top-level functions. ## Class-level generics If a static method signature makes use of a generic declared in its enclosing class, it must be moved to the function type. For example: ```[hack] class Wibble<Tclasslevel as arraykey> { public static function foo(Tclasslevel $x): Tclasslevel { return $x; } } function refEm(): (function(string): string) { $fptr = Wibble::foo<>; return $fptr; } ``` The expression `$fptr` will be given the polymorphic function type: ``` HH\FunctionRef<(readonly function<Tclasslevel as arraykey>(Tclasslevel $x): Tclasslevel)> ``` ## `where` constraints A `where` constraint expresses a restriction on a type parameter which may not be defined at the method level e.g. ``` class C<T> { public static function foo(): void where T as arraykey {} } function refEm(): void { $fptr = C::foo<>; } ``` Since `where ` constraints aren't part of the function type, we simplify then discharge any such contraints onto type parameters. This is always possible since we have closed over any required class-level type parameters; in the example we then have: ``` $fptr: HH\FunctionRef<(readonly function<T as arraykey>(): void)> ``` `where` constraints can also relate method-level generics to class-level generics and is further complicated by inheritance. ``` class Base<T> { public static function foo<Tu>(Tu $arg): T where T super vec<Tu> { return vec[$arg]; } } final class Derived<Ta> extends Base<vec<Ta>> {} function refEm(): void { $fptr = Derived::foo<>; } ``` ``` $fptr: HH\FunctionRef<(readonly function<Ta super Tu, Tu as Ta>(Tu $arg): vec<Ta>)> ``` Note that this typing is consistent with the already landed typing of generic top-level functions. ## `this` The type identifier `this` is used to refer to instance types of the lexically enclosing class. The type of function references to static methods which have `this` in their method signature will depend on the variance at which `this` occurs in the method signature. If `this` appears only contravariantly or covariantly, we can directly substitute the `this` for the instance type of the enclosing class, for example ```[hack] class Wibble<Tclasslevel as arraykey> { public static function contra_only(this $_): void{} public static function co_only(): this { ... } } function refEm(): void { $fptr = Wibble::contra_only<>; $gptr = Wibble::co_only<>; } ``` ``` $fptr : HH\FunctionRef<(readonly function<Tclasslevel as arraykey>(Wibble<Tclasslevel> $_): void)> $gptr : HH\FunctionRef<(readonly function<Tclasslevel as arraykey>(): Wibble<Tclasslevel>)> ``` Where `this` occurs invariantly we introduce a type parameter: ```[hack] class Wibble<Tclasslevel as arraykey> { public static function invariantly(this $_): this { ... } } function refEm(): void { $fptr = Wibble::invariantly<>; } ``` ``` $fptr: HH\FunctionRef<(readonly function<Tthis as Wibble<Tclasslevel>, Tclasslevel as arraykey>(Tthis $x): Tthis)> ``` Note that the variance of occurrences of `this` is determined by an analysis of the function signature and is 'aware' of the variance of type parameters in class declarations. In this example, - `this` appears invariantly in the signature of `fizz` since `Box` is invariant in its type parameter - `this` appears covariantly in the signature of `buzz` since `Contra` is contravariant in its type parameter but occurs in a contravariant position whilst `Co` is covariant in its type parameter and occurs in a covariant position. ```[hack] class Box<T> {} class Contra<-T> {} class Co<+T> {} class Wibble<Tclasslevel as arraykey> { public static function fizz(Box<this> $_): void {} public static function buzz(Contra<this> $_): ?Co<this>{ return null; } } ``` The logic for this part of typing is contained in the module `Typing_extract_method.This_variance`. ## Type constants Abstract type constants and concrete type constant aliasing abstract type constants significantly complicate the typing of function references to static methods. The logic for this part of typing is contained in the module `Typing_extract_method.Typeconst_analysis`. In order to 'extract' a method signature involving one of these constants from its containing class we make use of the following equivalence: ``` (exist T. U<T>) -> V === all T. (U<T> -> V) ``` To see how this applies, it's useful to think about the declaration of a class as being implicitly existentially quantified over all abstract type constants it contains. Moving these existentially quantified to type parameters and using class refinements to equate them allows us to be polymorphic in the type constant. For example: ```[hack] abstract class Wibble { abstract const type T as arraykey; public static function foo(this $_, this::T $_, Wibble $_): void {} } function refEm(): void { $fptr = Wibble::foo<>; } ``` ``` HH\FunctionRef<(readonly function<T#0 as arraykey>(Wibble with { type T = T#0 }, T#0, Wibble): void)> ``` Note that in this example, we are only refining the first parameter type (`this`), not the third (`Wibble`). The refinement works for arbitrary access paths, for example ```[hack] abstract class AbstractA { abstract const type TA as arraykey; } abstract class AbstractLeft { abstract const type TLeft as AbstractA; } abstract class AbstractRight { abstract const type TRight as AbstractA; } abstract class AbstractB { abstract const type TL as AbstractLeft; abstract const type TR as AbstractRight; } abstract class AbstractC { abstract const type TC as AbstractB; public static function tickle( this $_, this::TC $_, this::TC::TL $_, this::TC::TR $_, this::TC::TR::TRight $_, this::TC::TL::TLeft $_, this::TC::TR::TRight::TA $_, this::TC::TL::TLeft::TA $_, ): void {} } function refEm(): void { $fptr = AbstractC::tickle<>; } ``` ``` $fptr : HH\FunctionRef<(readonly function< TA0 as arraykey, TA1 as arraykey, TC0 as AbstractB with { type TL = TL0; type TR = TR0 }, TL0 as AbstractLeft with { type TLeft = TLeft0 }, TLeft0 as AbstractA with { type TA = TA0 }, TR0 as AbstractRight with { type TRight = TRight0 }, TRight0 as AbstractA with { type TA = TA1 } >( AbstractC with { type TC = TC0 }, TC0, TL0, TR0, TRight0, TLeft0, TA1, TA0 ): void)> ``` The analysis is also able to correctly type methods with abstract constants containing fix-point refinements: ``` abstract class AbstractA { abstract const type T as AbstractA with { type T = this::T; }; public static function foo(this $_): this::T { throw new Exception(); } } function refEm(): void { $fptr = AbstractA::foo<>; hh_show($fptr); } ``` ``` $fptr: HH\FunctionRef<(readonly function< T#0 as AbstractA with { type T = T#0 } >( AbstractA with { type T = T#0 } ): T#0)> ``` Reviewed By: andrewjkennedy Differential Revision: D74076920 fbshipit-source-id: e4304f2680ac4b3b5d23c42759a867d2ee84b3f0
1 parent d09da36 commit 2ff573c

30 files changed

+2173
-70
lines changed

hphp/hack/src/typing/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@
301301
(modules
302302
typing
303303
typing_class
304+
typing_extract_method
304305
typing_memoize
305306
typing_toplevel
306307
typing_typedef

0 commit comments

Comments
 (0)