Hack introduces generics to PHP (in the same vein as statically type languages such as C# and Java). Generics allow classes and methods to be parameterized (i.e., a type associated when a class is instantiated or a method is called). Here is an example:
Parameterization of a class or method provides the following, related, benefits:
Generics allow developers to write one class or method with the ability to be parameterized to any type, all while preserving type safety. Without a generics paradigm, to accomplish a similar model would require treating everything as a top-level object, many instanceof() checks, and casts to the appropriate type.
Like classes, traits and interfaces can be generic as well. The guidelines are similar to that of classes. Here is an example:
<?hh// Copyright 2004-present Facebook. All Rights Reserved.
// generic interfaceinterface Box<T> {
public function add(T $item): void;
public function remove(): T;
}
// generic traittrait Commerce<T> {
public function buy(T $item): void {
echo 'Bought a '.get_class($item)."\n";
}
public function sell(T $item): void {
echo 'Sold a '.get_class($item)."\n";
}
}
// generic class that uses generic trait and implements generic interfaceclass BestBuy<T> implements Box<T> {
protected Vector<T> $vec;
private int $last;
public function __construct() {
$this->vec = Vector{};
$this->last = -1;
}
use Commerce<T>;
public function add(T $item): void {
$this->vec->add($item);
$this->last++;
}
public function remove(): T {
$item = $this->vec->at($this->last);
$this->vec->removeKey($this->last--);
return $item;
}
}
// For example purposesabstract class Computer {}
class Apple extends Computer{}
class Lenovo extends Computer {}
class Dell extends Computer {}
function main_gti() {
$store = new BestBuy();
$store->add(new Lenovo());
$store->add(new Apple());
$store->add(new Dell());
echo get_class($store->remove())."\n";
$store->sell($store->remove());
}
main_gti();
The above example will output:
Writing a Generic Method ¶
When writing a generic method, keep the following guidelines in mind:
The generic parameter must start with a capital letter T.
The generic method must not collide with any existing, non-generic method name (i.e, public function swapand public function swap<T>).
Inside the generic method, T can be referred to for the return type.
Nullable T is supported (i.e., ?T).
Hack does not currently support for casting to T or creating new instances of T.
Here is an example of a generic method:
Generics and Type Inference ¶
Generics may have some surprising semantics when it comes to type inference. This code example will be used to discuss how generics and type inference are handled within Hack.
The bottom line here is that until this generic collection is exposed to code that takes or returns a type that will cause incompatibility, the type checker will be relaxed as to what it allows to be added.
Override on Return Type ¶
Hack brings about a feature to override on return type in a particular circumstance. In essence, a method can be overridden by return type in a subclass if the return type is strictly compatible with the return type of the same method in the superclass. So, this works:
Take a look at this seemingly similar example:
T<mixed> Compatibility ¶
mixed is sometimes a source of confusion when it comes to compatibility with other types. This confusion can become exacerbated when it comes to generics. For example, should this pass the Hack type checker?
Imagine if the code above was modified to look like this:
Even though Hack is able to catch a T<int> to T<mixed> conversion attempt before runtime, a cleaner way to write the above code is to use a generic method:
Constraints ¶
Generic class and methods can have restrictions to the types that are able to be used for the parameterized type argument upon instantiation or call, respectively. These are called constraints on type parameters. Here is an example:
Constraints are declared using the as keyword. In the above example, Identity is an abstract class with a parameterized type T. This class cannot be instantiated; thus, concrete class implementations must be created in order to access the functionality of Identity. Four final classes were created that extend Identity. AnyIdentity is a concrete implementation of Identity. It has no constraints. Any type may be passed in for T. The other three classes do have constraints:
CAIdentity has a constraint that only the type CA (and its children!) may be used for T.
CBIdentity has a constraint that only the type CB (and its children!) may be used for T.
FooIdentity has a constraint that only those types that implement the IFoo interface (and their children!) may be used for T.
For the above example, there will be two errors thrown by the Hack type checker. The first is on this line of code:
The second Hack error is on this line of code:
Here is an example of a constraint on a generic method:
A possible question that one may have when reading about constraints is, "Why have a constraint? Why not just have the class in question extend the class that is being used as the constraint?" Here is an example that addresses that question:
A couple of important notes about constraints and the Hack type checker:
Currently, Hack only allows one constraint on a type or a method.
Constraints cannot be used with primitive types. Since primitives are not inheritable, it wouldn't make sense in such a context. Just have the method take the primitive directly.
Open and Closed Types ¶
With generics, it is useful to discuss the concept of open and closed types. A type is open if it is a type parameter (e.g.T). Obviously enough, a type is closed if it is not open (e.g. int) . Here is an example of using open and closed types when it comes to generics:
What happens, though, when a method takes an open type and tries to return a closed type using that open type? Take this example using two generic methods:
How can these issues be resolved? These are possibilities, but should be used with caution:
Casting: bar() could cast $a as int before returning.
Return an open type: bar() can return a T instead of an int.
// UNSAFE: Use Hack's UNSAFE annotation to get around the type checker. Use with extreme caution.
Constraints: Assuming primitives aren't involved (they are not supported), constraints can be used to help guarantee a return type.
Rework code: Maybe if this situation arises, the code can be redesigned better.
Style Guidelines ¶
The following are recommended style guidelines for when using generics:
Begin all type parameters with T.
Name generic parameters with descriptive names (e.g., class Foo<TWrappedObject>), unless a single letter is self-explanatory or the generic parameter is a generic-generic parameter type.
When using a non-descriptive name, a generic class or method with one parameter should generally be named<T>.
When using a non-descriptive name, a generic class or method with two or more enumerated parameters should be named in the form of <Ta, Tb, ...> or <T1, T2,...>.
A collection type with a key and value (such as a Map) should have its generic parameters named <Tk, Tv>.