Know How - Programming - PHP

Extrem enervierend ungewöhnliches Verhalten vom Operator "?:"

Der Operator "?:" wie in "A ? B : C" verhält sich mehr als ungewöhnlich.

echo 0 ? 1 : 2 ? 3 : 4;
echo 1 ? 2 : 3 ? 4 : 5;
ergibt
3
4

In C ergibt folgendes
#include <stdio.h>
void
main(void)
{
  printf ("%d\n", 0 ? 1 : 2 ? 3 : 4);
  printf ("%d\n", 1 ? 2 : 3 ? 4 : 5);
}
das erwartete
3
2

Woher kommt der Unterschied?

PHP klammert so:
(1 ? 2 : 3) ? 4 : 5
während C so klammert wie man es erwartet:
1 ? 2 : (3 ? 4 : 5)

Warum erwartet man letzteres und nicht ersteres wie in PHP?

Ganz einfach, man erwartet einfach dass die Evaluierung folgender beider Konstrukte identisch abläuft:
if (A)  B; else if (C)  D; else E;
    A ? B :         C ? D :     E
Rein von der Theorie der Programmiersprachen her ist die zweite Schreibweise nämlich nichts anderes als die funktionale Schreibweise von ersterem.

Vergleichen wir mal folgendes:
if (true)   if (false) echo 1;   else echo 2;
Man könnte das auch als
if (true) { if (false) echo 1;   else echo 2; }
oder als
if (true) { if (false) echo 1; } else echo 2;
lesen, beides wäre syntaktisch richtig.

PHP folgt der ersteren Methode. Das nennt sich gemäß LALR(1) "Reduce early". Daraus resultiert auch, dass sie bei ?: konsequenterweise die C-Variante machen müssten. Machen sie aber nicht.

Hä? Machen sie doch, sie reduzieren zuerst den Term "A ? B : C" zu einem Ergebnis und berechnen dann "(A ? B : C) ? D : E".
Nö, machen sie nicht. Für den Menschen sieht es vielleicht so aus, aber Computer ticken anders.

Der entscheidende Punkt für den Parser ist, wenn er den ersten Doppelpunkt zu Gesicht bekommt! Der Reduce-Schritt wird also nicht erst nach dem Lesen von "C" durchgeführt, sondern schon beim Lesen des ":". Deshalb "Early".

Der Ablauf ist also folgender:

  • Evaluiere A
  • Ah, wir haben ein Conditional (?), also parsen wir B
  • Jetzt kommt der ':', also können wir JETZT schon reduzieren.
  • Sehen wir mal, was wir bisher haben: A und B.
  • Ist A true? Dann haben wir schon das Ergebnis B und werfen was kommt weg.
  • Ist A false? Dann schmeißen wir B weg und müssen den Rest berechnen.
  • Nun evaluieren wir C
  • Ah, wir haben ein Conditional (?), also parsen wir D
  • Jetzt kommt der ':', also können wir JETZT schon reduzieren.
  • Sehen wir mal, was wir bisher haben: C und D.
  • Ist C true? Dann haben wir schon das Ergebnis D und werfen was kommt weg.
  • Ist C false? Dann schmeißen wir D weg und müssen den Rest berechnen.
  • Nun evaluieren wir E
  • Jetzt haben wir das Ergebnis des zweiten Conditionals (C ? D : E)
  • Jetzt haben wir das Ergebnis des ersten Conditionals (A ? B : (C ? D : E))
Der Fehler ist also, dass PHP hier keinen Reduzierschritt beim ersten ":" macht, sondern erst nachdem es "C" gelesen hat.

Wie das bei PHP passieren konnte ist mir ein Rätsel. Ein LALR(1)-Parser macht es richtig. Ein naiver rekursiver Parser macht es ebenfalls richtig. Nur PHP macht es also anders. Hier eine Grammatik die so einen Mist verursachen kann:

cond := expr
     |  cond '?' cond ':' expr
     .
expr := /* eine Berechnung die kein ?: enthält */

Hier die "naive" Grammatik die es richtig machen würde:
cond := expr
     |  expr '?' cond ':' cond
     .
expr := /* eine Berechnung die kein ?: enthält */

Bemerke nur ich, dass die zweite Grammatik viel einfacher ist da sie keine Startrekursion (rekursiven Aufruf am Anfang) benötigt, sondern stattdessen über eine Endrekursion verfügt, die viel einfacher ist? Übrigens wird beides (Startrekursion wie Endrekursion) durch eine Iteration erledigt. Nur bei der Endrekursion wird die gesamte Funktion iteriert (was einfacher ist, man springt einfach zum Anfang), während bei der Startrekursion ein Zwischenschritt notwendig wird um es iterierbar zu machen:

cond := expr iter;
iter := '?' cond ':' expr | .
expr := /* eine Berechnung die kein ?: enthält */
(Das sieht zwar nicht besonders anders aus, aber man beachte, dass "iter" mit einem Literal beginnt und dann vollständig ausgeführt wird oder eben gar nicht ausgeführt wird. Das entspricht dem klassischen Pattern einer bedingten Iteration. In diesem Fall muss man aber leider das Value im Parse-Tree abwärts passen, was unangenehm ist.)

-Tino, 2008-03-17, Updated 2008-04-29
PS: Ja, stimmt, PHP dokumentiert verklausuliert, dass ?: linksassoziativ ist, die Klammern also auf der LINKEN Seite von ?: stehen. Sie zeigen sogar ein deutlich geklammertes Beispiel. Aber daraus schließt man trotzdem nicht auf das seltsame Verhalten dieses Operators. Ein überraschendes Verhalten einer beliebigen Sprache ist übrigens grundsätzlich ein Fehler. Fehler im Sprachdesign sind seltsam. Wen es nicht überrascht soll bitte nochmals das "if then else"-Beispiel lesen. Wenn es mehrere Möglichkeiten gibt etwas gleichartiges auszudrücken, dann muss es in jeder Schreibweise identisch ablaufen, anderenfalls ist es überraschend.