1unitedpower: PHP: Vorerst keine Union-Types.

Beitrag lesen

PHP ist traditionell eine deklarationsarme Sprache, in PHP 5 kamen die Type-Hints und in 7 die MÖGLICHKEIT zur Typdeklaration mit strikter Prüfung hinzu.

Außerdem kamen in PHP 7 Typehints für primitive Datentypen, und nullable und iterable Types, wie Sven schon sagte. Man hat schon viel für die Typechecking-Community getan - das ist auch PHP-Tradition.

Ob die strikten Typen, die PHP 7 kennt, den Compiler dazu bewegen, weniger Laufzeitprüfungen auf korrekte Wertetypen durchzuführen, wäre auch erstmal zu betrachten.

Das wäre für mich nur nebensächlich. Ich mag Typsysteme für die Hilfestellungen, die sie dem Programmierer bieten. Aber wenn dabei Performance-Boosts abspringen, ist das natürlich nett.

Andererseits, wenn ich mir den Proposal anschaue, ist der vorgeschlagene Anwendungszweck zumindest teilweise schrecklich. Es gibt Funktionen, die geben ein Objekt zurück oder FALSE im Fall eines Errors. Das können sie nur, weil Variablen in PHP prinzipiell ungetypt sind.

Ich stimme dir was die Beispiele angeht voll und ganz zu. Dein letzter Satz ist aber ein Fehlschluss. PHP ist typsicher, und deswegen hat jede Variable zur Laufzeit immer auch einen assoziierten Typen - das ist einfach der Typ des Wertes, den die Variable angenommen hat. Man kann diese Typen nicht selber für Variablen festlegen, aber man kann sie mittels Typinferenz herausfinden und mit einem Typchecker gewisse Invarianten für sie überprüfen. Mit Union-Types könnte man schließlich noch mehr Invarianten testen.

Es kennt zwar Wertetypen, aber Variablen können erstmal alles speichern. Eine Funktion, die je nach Ergebnis einen anderen Wertetyp liefert, ist aus meiner Sicht der komplette Horror.

Die Alternative ist oft noch viel schlimmer. Ich möchte mal selber ein Beispiel bringen, das für mich sehr gut funktioniert.

function divide (int $a, int $b) : float {
   return $a / $b;
}

Die Funktion teilt zwei Zahlen. Die Division durch 0 stellt eine Ausnahmefall da, den es nun zu behandeln gilt. Im obigen Code würde PHP Laufzeit-Fehler produzieren, wenn die Funktion aufgerufen wird. Das hat zur Folge, dass der Anwender der Funktion den Fehler abfangen muss. Das Üble dabei ist, dass die Funktions-Signatur das nicht ausdrückt, und ein Editor den Anwender auch nicht durch statische Codeanalyse darauf hinweise kann. Man überträgt dem Anwender der Funktion also auch gleich die Verantwortung Fehlerfälle zu behandeln.

Es wäre schöner, könnte man den Fehler schon innerhalb der Funktion bearbeiten. Dann stellt sich aber die Frage, was man nun im Fehlerfall zurück geben kann und da sind durch den Rückgabetypen float Grenzen gesetzt. Einfach eine willkürliche Zahl zurückgeben kann nicht die Lösung sein:

function divide (int $a, int $b) : float {
   if ($b === 0) {
      return 42; // Meh
   } else {
      return $a / $b;
   }
}

Die häufig auftretende Vorgehensweise in PHP ist, den Rückgabetypen der Funktion unspezifiziert zu lassen und bei Fehlschlag der Operation false zurück zu geben.

function divide (int $a, int $b) {
   if ($b === 0) {
      return false; // Meh
   } else {
      return $a / $b;
   }
}

Dann muss der Anwender der Funktion trotzdem noch überall zwei Fälle behandeln und die Editor-Unterstützung fehlt weiterhin für den Fehlerfall und nun auch noch für den positiven Fall. Mit Union-Types könnte man erzwingen, dass immer alle Fälle bei der Weiterverarbeitung abgedeckt sind. Und der Editor könnte beim Tippen der Funktion dem Anwender sofort eine Liste mit allen Fällen anbieten.

function divide (int $a, int $b) : float | bool {
   if ($b === 0) {
      return false;
   } else {
      return $a / $b;
   }
}

Das löst die Probleme. Das ließe sich noch schöner modellieren, wenn man auch komplexe Typen für Union-Types verwenden könnte.

function divide (int $a, int $b) : Success | Fail {
   if ($b === 0) {
      return new Fail('Division by zero');
   } else {
      return new Success($a / $b);
   }
}

Wie Sven richtig herausgestellt hat, wäre letzteres auch nicht mit der Akzeptanz des RFCs möglich geworden. Hier teile ich Svens Kritik.

Dazu noch mit der automatischen truthy/falsy Interpretation von non-boolean Werten, und dem Zwang, zwischen der Rückgabe eines falsy-Wertes und echtem FALSE unterscheiden zu müssen.

Typecoercing ist ein anderes Thema.

Solche Funktionen gehören auf den Misthaufen der Geschichte, und sie gehören nicht dadurch konserviert, dass man ihre Rückgaben mittels Union-Typisierung veredelt.

Gehen wir mal von der Hypothese aus, dass solche Funktionen, wie meine divide-Funktion dort oben, mit Union-Types in PHP möglich wären. Welche Gefahr siehst du von ihnen ausgehen? Und wie würdest du die divide-Methode heute ohne Union-Types modellieren?

Ein Rückgabetyp "int | false" ist meiner Meinung nach ein Designfehler. Eine Typisierung "ClassXYZ | null" ist auch nicht viel besser. Was fehlt, ist ein klarer Mechanismus zur Rückgabe von zwei Werten, und der Möglichkeit, das auch deklarieren zu können. Aber sowas geht nicht und es gibt nicht mal einen RfC dafür. Oder übersehe ich etwas, außer Referenz-Parametern als Ersatz für out-Parameter?

Du verlangst zwei Dinge: Tupel-Typen und Destructuring für Tupel-Typen. Für beides hast du mein Votum.

Das, was algebraische Typen sein KÖNNEN - soweit ich das aus dem englischen Wikipedia-Artikel herauslese - ist etwas ganz anderes.

Die Beispiele im Artikel sind alle in Haskell, das lässt es sehr unterschiedlich erscheinen. Konzeptionell geht es aber um das selbe. Die Beispiele lassen sich relativ simple in PHP übersetzen:

data List a = Nil | Cons a (List a)
class Nil {}
class Cons {
   protected $head;
   protected $tail; 
   public function __construct($value, Nil | Cons $tail) {
      $this->head = $value;
      $this->tail = $tail;
   }
}
data Tree = Empty
          | Leaf Int
          | Node Tree Tree
class Empty {}
class Leaf {
    protected $value;
    public function __construct(int $value) {
        $this->value = $value;
    }
}
class Node {
    protected $left;
    protected $right;
    public function __construct(Empty | Node | Leaf $left, Empty | Node | Leaf $right) {
        $this->left = $left;
        $this->right = $right;
    }
}

Mit generics, die ebenfalls proposed sind, könnte man auch den parametrischen Polymorphmus von Haskell in der PHP-Übersetzung unterbringen.

Ob das für PHP machbar ist, ohne Compiler und Runtime komplett neu zu schreiben, wäre erstmal zu überlegen. Von daher: 100% dafür, die Idee in dieser Formulierung abzulehnen.

Ein Pull-Request hat dem RFC beigelegen.