Traits ¶
Table of Contents ¶
Traits are "interfaces with implementation", that, at runtime, are "copied and pasted" into the class that uses the trait. The Hack type checker treats traits differently than HHVM. Hack treats traits as a stand-alone entity during the type checking process. In other words, it ensures type consistency within the trait (i.e., as a black box, so to speak), but does not "copy and paste" the code into all of the classes that use the trait and check for type consistency there. The reason this is done comes down to performance. It would be quite difficult to incrementally check Hack-enabled code in any performant way when trait code had to be inserted into a bunch of classes during the process.
Hack will correctly type check all the code within a trait:
<?hh
trait Foo {
private int $x = 5;
private bool $b = false;
protected function getVal(): int {
return $this->x;
}
public function getBin(Vector<bool>; $vec): string {
return $vec[0];
}
}
The above example will output:
File "traits.php", line 12, characters 12-18: Invalid return type File "traits.php", line 11, characters 46-51: This is a string File "traits.php", line 11, characters 33-36: It is incompatible with a bool
Hack will also throw an error on the following code, although the code runs perfectly fine.
<?hh
trait TTT {
protected function foo(): int {
return $this->bar();
}
}
class AAA {
protected function bar(): int {
return 1;
}
}
class BBB extends AAA {
use TTT;
public function baz(): int {
return $this->foo();
}
}
function main_t(): void {
$bbb = new BBB();
echo $bbb->baz();
}
main_t();
The above example will output:
File "traits.php", line 6, characters 19-24: The method bar is undefined in an object of type TTT
int(5)
Using $this in a trait on a method defined in the class where the trait will be used is not supported in Hack (remember traits are basically standalone entities to Hack). In order to remedy the above limitation from the Hack type checker,// UNSAFE could be used within foo(), or the file could be put in Hack's // decl mode. However, a better option is to use an abstract method within the trait. Traits support the use of abstract methods in order to impose requirements upon the class(es) where the trait will be used.
<?hhtrait TTT {
abstract protected function bar(): int;
protected function foo(): int {
return $this->bar();
}
}
class AAA {
protected function bar(): int {
return 1;
}
}
class BBB extends AAA {
use TTT;
public function baz(): int {
return $this->foo();
}
}
function main_t(): void {
$bbb = new BBB();
echo $bbb->baz();
}
main_t();
The above example will output:
No errors!
It is important to note that HHVM allows traits to implement interfaces (something not currently available in PHP), and Hack does support that feature with traits. The following example shows a trait implementing an interface.
<?hh
interface Bar {
public function babs(Vector<int> $vec): int;
public function get(): int;
}
trait Foo implements Bar {
private int $x = 5;
private function getVal(): int {
return $this->x;
}
public function get(): int {
return $this->getVal();
}
public function babs(Vector<int> $vec): int {
if (count($vec) < 0) {
return $vec[0];
}
return -1;
}
}
class Baz implements Bar {
use Foo;
private Vector<int> $v;
public function __construct() {
$this->v = Vector {};
$this->v[] = $this->get();
}
public function sass(): int {
return $this->babs($this->v);
}
}
function main_traits() {
$b = new Baz();
var_dump($b->sass());
}
main_traits();
The above example will output:
No errors!
int(5)
Notice how, in Baz, $this is being used to access methods that were defined in the trait Foo. Remember, Hack allows using $this on such methods since traits are basically checked as a standalone entity; in this case, the trait has type consistency.
Trait Requirements ¶
The Hack typechecker is effective because it is able to take annotated facts sprinkled across the codebase in logical places (function entry and exit points, class definitions, etc), throw them together to reconcile them, and detect any mismatches. Traits are a sizeable wrench in the works, because the nature of traits in PHP is "declare a bunch of stuff to be thrown into another scope at a later time, then check if it works at runtime", whereas the typechecker is only happy if it can take a piece of code and statically verify it independently of its usage sites. A syntactic way of having traits state that they only apply inside a particular type hierarchy allows their declarations to be both more explicit and more amenable to static analysis.
While traits can theoretically be used for aspect oriented programming to add functionality to a class that is entirely orthogonal to the rest of the class definition, in practice it seems that a major, if not the dominant, use case for traits is to provide an implementation of functionality that applies only within the context of a particular class hierarchy. For example, if there are 50 subclasses of X, and X::foo() has 3 different recommended implementations, the 3 traits that make those implementations possible are meaningless outside of the class hierarchy and over time may start to justifiably use the functionality of X.
Trait requirement declarations serve to tie traits to class hierarchies and move the issues of assumptions about the using class from being discoverable only at run time to being easily discoverable in static analysis. In the example above, T's trait requirements indicate that I is something that classes using T are required to implement.
For the purposes of the Hack typechecker, trait requirements now mean that any functions inside T above now know that $this instanceof C and $this instanceof I are true, and duplicating the implementations of C and I using redundant abstract function declarations is no longer unnecessary.
<?hh// Definitions of class C and interface Iclass C {}
interface I {
public function foo();
}
// Trait T requires that any using class is a descendent of class C and implements
// interface Itrait T {
require extends C;
require implements I;
// ... trait functionality that potentially relies on $this extending C and implementing I ...
// ... this functionality can be safely typechecked even if there are no classes that use T !}
When enforcing trait requirements, all T cares about is that X instanceof C is true and X instanceof I is true for any using class X. It doesn't care about how exactly compliance with X's declared interfaces is achieved, or how many layers of inheritance there might be between X and C.
<?hh// Classes X1, X2, X3, and X4 all satisfy T's requirements:class X1 extends C implements I {
use T;
public function foo() { .. } // I satisfied via inline implementation}
trait U {
public function foo() { .. }
}
class X2 extends C implements I {
use T;
use U; // I satisfied via another trait}
interface J extends I {}
class X3 extends C implements J { // interface inheritance is fine
use T;
public function foo() { .. } // with an inline implementation in this case}
class D extends C implements I {
public function foo() { .. }
}
class X4 extends D { // number of inheritance hops wouldn't matter to instanceof ...
use T; // ... so they don't matter to trait requirements}
// Classes X5, X6, and X7 do not satisfy all of T's requirementsclass X5 { // neither extends C nor implements I
use T;
}
class X6 extends C { // doesn't implement I
use T;
}
class X7 implements I { // doesn't extend C
use T;
public function foo() { .. }
}
// While class X8 provides a compatible implementation for all of interface I's methods,
// it is not considered to satisfy T's requirements because it does not formally implement
// interface Iclass X8 extends C {
use T;
use U;
}
// Class X9 would technically satisfy T's requirements if it were a valid declaration
// ... but it's not! It will fail to load at run time because it doesn't provide an
// implementation for I::foo()class X9 extends C implements I {
use T;
}
No comments:
Post a Comment