Pages

Wednesday, March 26, 2014

Trait Requirements

Trait Requirements 

The Hack typechecker is effective because it is able to take annotated facts sprinkled across a codebase (function entry and exit points, class definitions, etc), reconcile them, and detect any mismatches. Traits are a sizeable wrench in the works. The nature of traits in PHP is to "declare a bunch of stuff to be thrown into another scope at a later time, then check if it works at runtime"; the Hack 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.
<?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;
}
So what is the rationale for trait requirements? 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 type checker, 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 necessary.

No comments:

Post a Comment