Pages

Wednesday, March 26, 2014

Traits

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->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 {}
interface 
{
  public function 
foo();
}
 
// Trait T requires that any using class is a descendent of class C and implements
// interface I
trait {
  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 implements {
  use 
T;
  public function 
foo() { .. } // I satisfied via inline implementation}
 
trait 
{
  public function 
foo() { .. }
}
class 
X2 extends implements {
  use 
T;
  use 
U// I satisfied via another trait}
 
interface 
extends {}
class 
X3 extends implements // interface inheritance is fine
  
use T;
  public function 
foo() { .. } // with an inline implementation in this case}
 
class 
extends implements {
  public function 
foo() { .. }
}
class 
X4 extends // 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 // doesn't implement I
  
use T;
}
 
class 
X7 implements // 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 I
class X8 extends {
  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 implements {
  use 
T;
}






No comments:

Post a Comment