Classes: Using A Trait

Traits are a mechanism for code reuse that overcomes some limitations of Hack single inheritance model.

In its simplest form a trait defines properties and method declarations. A trait cannot be instantiated with new, but it can be used inside one or more classes, via the use clause. Informally, whenever a trait is used by a class, the property and method definitions of the trait are inlined (copy/pasted) inside the class itself. The example below shows a simple trait defining a method that returns even numbers. The trait is used by two, unrelated, classes.

trait T {
  public int $x = 0;

  public function return_even() : int {
    invariant($this->x % 2 == 0, 'error, not even\n');
    $this->x = $this->x + 2;
    return $this->x;
  }
}

class C1 {
  use T;

  public function foo() : void {
    echo "C1: " . $this->return_even() . "\n";
  }
}

class C2 {
  use T;

  public function bar() : void {
    echo "C2: " . $this->return_even() . "\n";
  }
}

<<__EntryPoint>>
function main() : void {
  (new C1()) -> foo();
  (new C2()) -> bar();
}
Output
C1: 2
C2: 2

A class can use multiple traits, and traits themselves can use one or more traits. The example below uses three traits, to generate even numbers, to generate odd numbers given a generator of even numbers, and to test if a number is odd:

trait T1 {
  public int $x = 0;

  public function return_even() : int {
    invariant($this->x % 2 == 0, 'error, not even\n');
    $this->x = $this->x + 2;
    return $this->x;
  }
}

trait T2 {
  use T1;

  public function return_odd() : int {
    return $this->return_even() + 1;
  }
}

trait T3 {
  public static function is_odd(int $x) : bool {
    if ($x % 2 == 1) {
      return true;
    } else {
      return false;
    }
  }
}

class C {
  use T2;
  use T3;

  public function foo() : void {
    echo static::is_odd($this->return_odd()) . "\n";
  }
}

<<__EntryPoint>>
function main() : void {
  (new C()) -> foo();
}
Output
1

Traits can contain both instance and static members. If a trait defines a static property, then each class using the trait has its own instance of the static property.

A trait can access methods and properties of the class that uses it, but these dependencies must be declared using trait requirements.

Resolution of naming conflicts

A trait can insert arbitrary properties and methods inside a class, and naming conflicts may arise. Conflict resolution rules are different according whether the conflict concern a property or a method.

If a class uses multiple traits that define the same property, say $x, then every trait must define the property $x with the same type, visibility modifier, and initialization value. The class itself may, or not, define again the property $x, subject to the same conditions as above.

Beware that at runtime all the instances of the multiply defined property $x are aliased. This might be source of unexpected interference between traits implementing unrelated services: in the example below the trait T2 breaks the invariant of trait T1 whenever both are used by the same class.

trait T1 {
  public static int $x = 0;

  public static function even() : void {
    invariant(static::$x % 2 == 0, 'error, not even\n');
    static::$x = static::$x + 2;
  }
}

trait T2 {
  public static int $x = 0;

  public static function inc() : void {
    static::$x = static::$x + 1;
  }
}

class C {
  use T1;
  use T2;

  public static function foo() : void {
    static::inc();
    static::even();
  }
}

<<__EntryPoint>>
function main() : void {
  try {
    C::foo();
  } catch (\Exception $ex) {
    echo "Caught an exception\n";
  }
}
Output
Caught an exception

For methods, a rule of thumb is "traits provide a method implementation unless the class itself does not". If the class implements a method m, then traits used by the class can define methods named m provided that their interfaces are compatible (more precisely super types of the type of the method defined in the class. At runtime methods inserted by traits are ignored, and dispatch selects the method defined in the class.

If multiple traits used by a class define the same method m, and a method named m is not defined by the class itself, then the code is rejected altogether, independently of the method interfaces.