0251 / 590 837 15

Tutorial: Erste Schritte mit Facebooks „Hack“

Facebook + PHP = Hack

Danke Facebook! Mit eurer Programmiersprache Hack seit ihr auf dem Weg PHPs größte Stärke und Schwäche aufzulösen – Der lockere Umgang mit Datentypen. Gerade für Programmier-Anfänger ist PHP geeignet, da man sich nicht mit den Datentypen rumschlagen muss.

In der professionellen PHP-Entwicklung muss man allerdings auch immer wieder darauf achten, dass die Parameter die man benutzt auch das Format haben, was man gerade haben möchte. Und genau hier setzt Facebooks Programmiersprache an. Sie basiert auf PHP und bringt Typensicherheit mit sich.

Auf der entsprechenden Website von Hack gibt es bereits ein Tutorial, was ich mit euch einfach mal auf Deutsch durchbeschreiben möchte.

1. PHP-Tag umbenennen

Facebook hat „Hack“ seinen eigenen Tag zugewiesen. Also auf Wiedersehen <?php hallo <?hh:

Vorher:

<?php
// ^-- FIXME: replace <?php with <?hh

// A Hack file always starts with <?hh

Nachher:

<?hh
// ^-- FIXME: replace <?php with <?hh

// A Hack file always starts with <?hh

2. Typehints einbauen

Im nächsten Schritt wird angegeben, in welchem Typ Parameter einer Funktion übergeben bzw. zurückgeliefert werden:

Vorher:

<?hh

// Hack functions are annotated with types.
function my_negation(bool $x): bool {
  return !$x;
}

// FIXME: annotate this function parameter
// and return with the type 'int'.
function add_one(/* TODO */ $x): /* TODO */ {
  return $x+1;
}

Nachher:

<?hh

// Hack functions are annotated with types.
function my_negation(bool $x): bool {
  return !$x;
}

// FIXME: annotate this function parameter
// and return with the type 'int'.
function add_one(int $x): int {
  return $x+1;
}

3. Type-Casting wird wichtiger

Die Parameter müssen nun natürlich auch im richtigen Typ übergeben werden. Haben wir also einen String als Basis und möchten ein Int haben, muss zuerst ein Typehint erfolgen, damit das Script läuft.

Vorher:

<?hh

/* Hack errors come in multiple parts.
 * Hover over the underlined parts!
 */

function add_one(int $x): int {
  return $x+1;
}

function test(): void {
  $my_string = 'hello';

  // Some clever code ...

  add_one($my_string);
}

Nachher:

<?hh

/* Hack errors come in multiple parts.
 * Hover over the underlined parts!
 */

function add_one(int $x): int {
  return $x+1;
}

function test(): void {
  $my_string = 'hello';

  // Some clever code ...

  add_one((int) $my_string);
}

4. NULL als Wert erlauben

Wer einen NULL-Wert als Übergabe erlauben möchte kann ein Fragezeichen vor dem Typehint packen.

Vorher:

<?hh

// Prefixing a type with '?' permits null.

// TODO: fix the type of the parameter $x to permit null.
function f(int $x): void {
  var_dump($x);
}

function test(): void {
  f(123);
  f(null);
}

Nachher:

<?hh

// Prefixing a type with '?' permits null.

// TODO: fix the type of the parameter $x to permit null.
function f(?int $x): void {
  var_dump($x);
}

function test(): void {
  f(123);
  f(null);
}

5. Nutzung von Interfaces mit Typehint

Auch bei Interfaces kann man angeben, welcher Typehint erwartet wird.

Vorher:

<?hh

interface User { public function getName(): string; }

function get_user_name(?User $user): string {

  if($user !== null) {
    // We checked that $user was not null.
    // Its type is now 'User'.

    /* TODO: return $user->getName() */
  }
  return '<invalid name>';
}

function test(User $user) {
  $name1 = get_user_name($user);
  $name2 = get_user_name(null);
}

Nachher:

<?hh

interface User { public function getName(): string; }

function get_user_name(?User $user): string {

  if($user !== null) {
    // We checked that $user was not null.
    // Its type is now 'User'.

    return $user->getName();
  }
  return '<invalid name>';
}

function test(User $user) {
  $name1 = get_user_name($user);
  $name2 = get_user_name(null);
}

6. Kein Rückgabewert, sondern Exceptions!

Der Rückgabewert muss immer übereinstimmen. Das heißt, tritt ein Fehler auf sollte man eine Exception schmeißen, statt einfach false oder null zurückzugeben:

Vorher:

<?hh

interface User { public function getName(): string; }

// There are many ways to handle null values.
// Throwing an exception is one of them.

function get_user_name(?User $user): string {

  if($user === null) {
    throw new RuntimeException('Invalid user name');
  }
  /* TODO: return $user->getName() */
}

function test(User $user) {
  $name1 = get_user_name($user);
  $name2 = get_user_name(null);
}

Nachher:

<?hh

interface User { public function getName(): string; }

// There are many ways to handle null values.
// Throwing an exception is one of them.

function get_user_name(?User $user): string {

  if($user === null) {
    throw new RuntimeException('Invalid user name');
  }
  return $user->getName();
}

function test(User $user) {
  $name1 = get_user_name($user);
  $name2 = get_user_name(null);
}

7. Die neuen Arrays – Vectoren

Hack hat seine eigenen Arten von Arrays. Diese sind im Gegensatz zu PHP typensicher und nennen sich Vectoren.
Sie sind Arrays mit numerischen Index:

<?hh

// Hack introduces new collection types (Vector, Set and Map).
function test(): int {

  // Vector is preferred over array(1, 2, 3)
  $vector = Vector {1, 2, 3};

  $sum = 0;
  foreach ($vector as $val) {
    $sum += $val;
  }

  return $sum;
}

8. Vectoren typensicher machen

Man kann dem Vector nun sagen, dass dieser nur Strings enthalten darf. Landet da dann eine Zahl, wird ein Fehler geworfen.

Vorher:

<?hh

// Hack uses generics for Collection types.

// TODO: fix the return type of the function 'test'
function test(): Vector<string> {
  $vector = Vector {1, 2, 3};
  return $vector;
}

Nachher:

<?hh

// Hack uses generics for Collection types.

// TODO: fix the return type of the function 'test'
function test(): Vector<string> {
  $vector = Vector {"1", "2", "3"};
  return $vector;
}

9. Die map-Funktionen – Um Schleifen zu sparen

Über die map-Syntax kann man sich foreach-Schleifen teilweise sparen. So kann man zum Beispiel alle Werte mit eins addieren lassen:

Vorher:

<?hh

function vector_add1(Vector<int> $v): Vector<int> {
  // Example of lambda expressions.
  return $v->map($x ==> $x + 1);
}

function vector_mult2(Vector<int> $v): Vector<int> {
  // TODO: write a function multiplying all the elements by 2
}

Nachher:

<?hh

function vector_add1(Vector<int> $v): Vector<int> {
  // Example of lambda expressions.
  return $v->map($x ==> $x + 1);
}

function vector_mult2(Vector<int> $v): Vector<int> {
  // TODO: write a function multiplying all the elements by 2
  return $v->map($x ==> $x * 2);
}

10. Klassenvariablen müssen intialisiert werden

Sehr gut! Die Klassenvariablen müssen alle spätestens im Konstruktor initialisiert werden, damit der Code läuft.

Vorher:

<?hh

// All the members of a class must be initialized

class Point {

  private float $x;
  private float $y;

  public function __construct(float $x, float $y) {
    $this->x = $x;
    // FIXME: initalize the member 'y'
  }
}

Nachher:

<?hh

// All the members of a class must be initialized

class Point {

  private float $x;
  private float $y;

  public function __construct(float $x, float $y) {
    $this->x = $x;
    $this->y = $y;
  }
}

11. Klassenvariablen im Konstruktor festlegen

Mit Facebooks Programmiersprache kann man die Deklaration der Eigenschaften einfach im Konstruktor festlegen, wo auch die Werte intialisiert werden:

<?hh

// Check out this new syntax!
// It's shorter and does same thing ...

class Point {

  public function __construct(
    private float $x,
    private float $y
  ) {}
}

12. Eigenes Generics

Man kann seine eigenen generischen Klassen erstellen. Nehmen wir als Basis die Vector-Klasse. Dabei konnten wir ja angeben, welchen Typ wird in dem Array erwarten. Sowas kann man auch für seine eigenen Klassen machen. Das heißt der Klasse selbst ist es egal, welcher Datentyp verwendet wird, in der Benutzung einer Instanz mit der Klasse muss der Datentyp aber immer gleich sein. So wird im folgenden Beispiel immer mit int gearbeitet, weil das bei der Instanzierung der Klasse angefordert wurde:

Vorher:

<?hh

// You can create your own generics!
class Store<T> {
  public function __construct(private T $data) {}
  public function get(): T { return $this->data; }
  public function set(T $x): void { $this->data = $x; }
}

// TODO: fix the return type of the function test
function test(): Store<int> {
  $data = 'Hello world!';
  $x = new Store($data);
  return $x;
}

Nachher:

<?hh

// You can create your own generics!
class Store<T> {
  public function __construct(private T $data) {}
  public function get(): T { return $this->data; }
  public function set(T $x): void { $this->data = $x; }
}

// TODO: fix the return type of the function test
function test(): Store<string> {
  $data = 'Hello world!';
  $x = new Store($data);
  return $x;
}

13. Arbeiten mit Interfaces

In Hack kann man bei Funktionen ein Interface erwarten und angeben, dass der Rückgabewert den gleichen Typ hat, wie die das übergebene Objekt.

Vorher:

<?hh

// You can specify constraints on generics.

interface MyInterface {
  public function foo(): void;
}

// TODO: uncomment 'as MyInterface'
// T as MyInterface means any object as long as
// it implements MyInterface
function call_foo<T /* as MyInterface */>(T $x): T {
  $x->foo();
  return $x;
}

Nachher:

<?hh

// You can specify constraints on generics.

interface MyInterface {
  public function foo(): void;
}

// TODO: uncomment 'as MyInterface'
// T as MyInterface means any object as long as
// it implements MyInterface
function call_foo<T as MyInterface>(T $x): T {
  $x->foo();
  return $x;
}

14. Vererbung: Rückgabewert = Geerbte Klasse

In Sachen Vererbung kann man bei den Funktionen angeben, dass ein Rückgabewert immer die Klasse der aktuell untersten Ebene ist. Das geht ganz einfach über this.

Vorher:

<?hh

// The type 'this' always points to the most derived type
class MyBaseClass {
  protected int $count = 0;

  // TODO: replace 'MyBaseClass' by 'this'
  public function add1(): MyBaseClass {
    $this->count += 1;
    return $this;
  }
}

class MyDerivedClass extends MyBaseClass {
  public function print_count(): void { echo $this->count; }
}

function test(): void {
  $x = new MyDerivedClass();
  $x->add1()->print_count();
}

Nachher:

<?hh

// The type 'this' always points to the most derived type
class MyBaseClass {
  protected int $count = 0;

  // TODO: replace 'MyBaseClass' by 'this'
  public function add1(): this {
    $this->count += 1;
    return $this;
  }
}

class MyDerivedClass extends MyBaseClass {
  public function print_count(): void { echo $this->count; }
}

function test(): void {
  $x = new MyDerivedClass();
  $x->add1()->print_count();
}

15. Abkürzungen bei Typendeklarationen

Wer lange Typenbeschreibungen hat, kann sie einfach über einen Alias abkürzen lassen:

<?hh

// When a type is too long, you can use a type alias.
type Matrix<T> = Vector<Vector<T>>;

function first_row<T>(Matrix<T> $matrix): Vector<T> {
  return $matrix[0];
}

16. Key-Value Werte

Hack ermöglicht es auch Key-Value Werte als Parameter zu übergeben:

Vorher:

<?hh

// Tuples represent fixed size arrays.
// TODO: fix the return type.
function my_first_pair((int, bool) $pair): int {
  list($_, $result) = $pair;
  return $result;
}

Nachher:

<?hh

// Tuples represent fixed size arrays.
// TODO: fix the return type.
function my_first_pair((int, bool) $pair): int {
  list($result,$_) = $pair;
  return $result;
}

17. Shapes – Statische Datensätze

Shapes sind quasi Klassne ohne Funktionen. Man kann angeben welche Datentypen eine solche Datensammlung beinhalten kann.

Vorher:

<?hh

// Shapes can be used for arrays with constant string keys.
type my_shape = shape(
  'field1' => int,
  'field2' => bool,
);

function first_shape(): my_shape {
  $result = shape('field1' => 1);
  
  // TODO: set 'field2' to the value true
  // on $result to complete the shape.
  return $result;
}

Nachher:

<?hh

// Shapes can be used for arrays with constant string keys.
type my_shape = shape(
  'field1' => int,
  'field2' => bool,
);

function first_shape(): my_shape {
  $result = my_shape(1,false);
  
  // TODO: set 'field2' to the value true
  // on $result to complete the shape.
  return $result;
}

18. Arbeiten mit Closures

Closures, als anonyme Funktionen die wie auch in JavaScript mit als Parameter übergeben werden können ebenfalls typensicher aufgerufen werden.

Vorher:

<?hh

// You can specify the types of functions too.
function apply_int<T>((function(int): T) $callback, int $value): T {
  // TODO: return $callback($value)
}

Nachher:

<?hh

// You can specify the types of functions too.
function apply_int<T>((function(int): T) $callback, int $value): T {
  return $callback($value);
}

19. Templates in Hack

Der Übergang von PHP zu HTML wurde von vielen Entwicklern mit Smarty getrennt. In Hack gibt es bereits integrierte Möglichkeiten mit Escapes zu arbeiten. So kann man zum Beispiel auch einfach ein div als Rückgabewert definieren:

<?hh

// XHP is useful to build html (or xml) elements.
// The escaping is done automatically, it is important to avoid
// security issues (XSS attacks).

function build_paragraph(string $text, string $style): :div {
  return
    <div style={$style}>
      <p>{$text}</p>
    </div>;
}

20. Aliase für Typen

Man kann zum Beispiel definieren, dass eine user_id immer vom Typ int ist. Diese kann man dann im kompletten Projekt für Typehint verwenden. Vorteil: Möchte man diese später mal zum String ändern muss man nur eine Stelle ändern!

<?hh

/* Opaque types let you hide the representation of a type.
 *
 * The definition below introduces the new type 'user_id'
 * that will only be compatible with 'int' within this file.
 * Outside of this file, 'user_id' becomes "opaque"; it won't
 * be compatible with 'int' anymore.
 */
newtype user_id = int;

function make_user_id(int $x): user_id {
  // Do some checks ...
  return $x;
}

// You should only use this function for rendering
function user_id_to_int(user_id $x): int {
  return $x;
}

21. Überschriebene Funktionen müssen mit Overwrite gekennzeichnet werden

Ich hatte mal einen sehr tollen Fehler, dass in einer abstrakten Klasse eine Funktion angelegt wurde, die in der Unterklasse bereits existiert hat. Ein sehr ärgerlicher Fehler, weil beide Funktionen völlig verschiedene Sachen gemacht haben => Es wurde aber immer nur die Funktion der Unterklasse aufgerufen. Gerade aus diesem Grund gefällt es mir, dass man überschriebene Funktionen extra kennzeichnen muss. Diese Funktion muss dann auf jeden Fall auch in der Basisklasse vorkommen.

Vorher:

<?hh

class MyBaseClass {
  // TODO: fix the typo in the name of the method.
  public function get_uuser(): MyUser {
    return new MyUser();
  }
}

class MyDerivedClass extends MyBaseClass {
  /* <<Override>> is used to specify that get_user has been inherited.
   * When that's not the case, Hack gives an error.
   */
  <<Override>> public function get_user(): MyUser {
    return new MyUser();
  }
}

Nachher:

<?hh

class MyBaseClass {
  // TODO: fix the typo in the name of the method.
  public function get_user(): MyUser {
    return new MyUser();
  }
}

class MyDerivedClass extends MyBaseClass {
  /* <<Override>> is used to specify that get_user has been inherited.
   * When that's not the case, Hack gives an error.
   */
  <<Override>> public function get_user(): MyUser {
    return new MyUser();
  }
}

22. Traits

Bei Traits ist es möglich anzugeben, dass diese nur in bestimmten Unterklassen genutzt werden können. Das geht über require.

Vorher:

<?hh

class C { protected function bar(): void {} }
interface I { public function foo(): void; }
 
// 'require' lets you specify what the trait needs to work properly.
trait T {

  // The class using the trait must extend 'C'
  require extends C;

  // TODO: uncomment the next line to fix the error
  //require implements I;

  public function do_stuff(): void {
    $this->bar(); // We can access bar because we used "require extends"
    $this->foo();
  }
}

Nachher:

<?hh

class C { protected function bar(): void {} }
interface I { public function foo(): void; }
 
// 'require' lets you specify what the trait needs to work properly.
trait T {

  // The class using the trait must extend 'C'
  require extends C;

  // TODO: uncomment the next line to fix the error
  require implements I;

  public function do_stuff(): void {
    $this->bar(); // We can access bar because we used "require extends"
    $this->foo();
  }
}

Mein Fazit: Lasst uns alle Hacker werden!

Also mich hat Hack extrem begeistert. Es löst fast alle Sachen, die mich aktuell an PHP stören. Wenn es Facebook geschickt macht, könnte Hack PHP im professionellen Bereich komplett ersetzen. Zum Lernen und Herumspielen kann dann PHP genutzt werden und später dann auf Hack umgestiegen werden.

Was meint ihr? Hat das Zukunft?

Vewandte Artikel

Kommentare

Henning schrieb am 25.03.2014:

Alles schön und gut aber das was Hack ausmacht beschreibst du nicht oder gehst nicht weiter darauf ein. Hack wird durch einen JIT-Compiler in der HHVM gejagt das ganze rennt also in einer VM wie bei Java.Zweitens viele dieser Sachen die nun vorgabe sind durch die Sprache sind meiner Meinung nach guter Programmierstyle und sollte sowieso gemacht werden. Exceptions werfen anstelle von Rückgabewerten z.B.

JuKu schrieb am 02.06.2014:

Ich glaube auch nicht, dass Hack PHP ersetzen wird oder kann. Bei PHP hat man mehr Spielraum, bei Hack Sicherheiten. Aber PHP ist gerade wegen diesem losen Umgang mit Datentypen so beliebt. Und wie schon gesagt wurde kann man bei PHP diese Sicherheiten mit gutem Programmierstyle ebenfalls schaffen.