am 10. März 2010
Ich nutze gleich einmal Nils Abwesenheit (Urlaub)
und verbiete Variablen, globale Zustände, globale Variablen, Schleifen und Referenzübergaben bei Funktionsaufrufen. Auch wenn sich jetzt alle Leser Nils zurückwünschen, diese Einschränkungen ergeben Sinn und ermöglichen sehr effizient zu programmieren. Machbar ist dies durch das zur imperativen Welt sehr unterschiedliche Paradigma der funktionalen Programmierung (basierend am Lambda-Kalkül), das auch immer mehr in imperativen Sprachen, wie zum Beispiel PHP, integriert wird. Neben Vorteilen, die die Programmierung vereinfachen, ist das bereits sehr alte Konzept (erste Grundlagen 1936) auch bestens für die Zukunft mit Multiprozessoren und Cloud-Computing gerüstet, da eine Parallelisierung beinahe automatisch erfolgen kann. Näheres jedoch im Laufe des Artikels.
Beginnen wir zuerst einmal mit den größten praktischen Unterschieden (zum leichteren Verständnis mit PHP Code) ganz nach dem Motto: “Alles ist eine Funktion”.
Keine Variablen? Wie kann ich dann ohne Variablen x + y rechnen, wenn z.B. x=3 und y=2?
Da wir nur Funktionen zur Verfügung haben, erstellen wir für die Addition einfach 3 Funktionen:
1. Funktion mit dem Namen x:
function x() { return 3; }
2. Funktion mit dem Namen y:
function y() { return 2; }
3. Funktion mit dem Namen +:
function plus($x,$y) { return $x + $y; }
function result() { return plus(x(),y()); }
Die Funktionen x und y sind daher Funktionen, die immer einen bestimmten konstanten Wert zurückgeben. Dieser Wert kann jedoch im Gegensatz zu Variablen nicht mehr geändert werden. Das Ergebnis result ist durch den Aufruf der Funktion plus mit den Parametern x und y definiert. Wir haben also die Berechnung ohne Variablen durchgeführt. Natürlich ist eine derartige Konstruktion in PHP nur bedingt sinnvoll, soll aber zeigen, dass der Verzicht auf Variablen auch grundsätzlich in PHP technisch möglich ist.
Keine Zustände oder globalen Variablen?
In der funktionalen Welt ist es sehr wichtig, dass Funktionen beim Aufruf mit gleichen Parametern (Input) immer den selben Wert zurückliefern (Output). Egal zu welchen Zeitpunkt im Programm oder auf welchem Rechner ausgeführt, muss eine Funktion bei einem bestimmten Input immer den selben Output haben.
$GLOBALS['einwert'] = 0;
function garNichtFunktional($x) {
$GLOBALS['einwert'] += 1;
return $x + $GLOBALS['einwert'];
}
Bei dieser Funktion wird bei zwei aufeinander folgenden Aufrufen nicht der selbe Rückgabewert errechnet, da sich zwischen zwei Aufrufen der globale Wert einwert ändert. So liefert garNichtFunktional(4) zuerst 5 als Ergebnis und bei einem erneuten Aufruf das Ergebnis 6. Die Funktion wird in diesem Beispiel von außen (der globalen Variable) beinflusst. Genau aus diesem Grund, um eine Abhängigkeit von einem Bereich außerhalb der Funktion zu verhindern, ist es ebenfalls nicht möglich, eine Referenz als Parameter zu übergeben. Erhält eine Funktion eine Referenz, könnte die Funktion einen Zustand/Wert außerhalb ihres Bereiches über die Referenz verändern und damit andere Funktionen bzw. Zustände beeinflussen. Diese Veränderung von Bereichen außerhalb der Funktion ist unter dem Begriff Seiteneffekte bekannt. Um Seiteneffekte zu verhindern, wird daher nur das Call-by-Value Konzept angewandt. Eine Funktion kann daher nur über die Parameter und dem Return-Wert mit der “Außenwelt” kommunizieren.
Keine Schleifen?
Da Schleifen Zählvariablen besitzen, sind diese in strengen funktionalen Konzepten ebenfalls nicht erlaubt. Ersetzt wird die Mächtigkeit von Schleifen durch die Rekursion. Die folgende while Schleife
$x = 1;
while ($x <= 10) {
$x += 1;
}
kann zum Beispiel rekursiv durch die folgende Funktion abgebildet werden.
function bisZehn($x) {
if ($x >= 10) return $x;
else return bisZehn($x+1);
}
$x = bisZehn(1);
Funktionen als Parameter?
Da es keine klassischen Variablen gibt und jeder Ausdruck als auswertbare Funktion angesehen wird, können als Parameter nur Funktionen übergeben werden. Dieses Feature (sogenannte Funktionen höherer Ordnung oder Higher-Order Functions) ist auch seit der PHP Version 5.3 vorhanden und ermöglicht zum Beispiel folgende Konstrukte:
function array_map($array,$funktion) {
for ($i=0; $i < count($array); $i++) {
$array[$i] = $funktion($array[$i]);
}
return $array;
}
//oder streng funktional ohne for Schleife mit Rekursion implementiert
function array_map($array,$funktion,$index) {
if ($index >= count($array)) {
return array();
}
return array_merge(
array($funktion($array[$index])),
array_map($array,$funktion,$index+1)
);
}
array_map($mein_array,function($element) { return $element+5; },0);
array_map($mein_array,function($element) { return md5($element); },0);
Die Funktion array_map wendet dabei eine beliebige übergebene Funktion auf das übergebene Array an. Der erste Aufruf würde zum Beispiel zu jedem Element 5 addieren. Der zweite Aufruf übergibt eine anonyme Funktion, die den md5 Hash Wert für einen Parameter $element berechnet, und liefert daher als Gesamtergebnis das Array mit den md5 Hash Werten der originalen Array-Elemente.
Der große Vorteil besteht hierbei in der flexiblen Wiederverwendbarkeit der Funktion array_map. Diese kann mit beliebigen Funktionen kombiniert werden und spart viel Programmierarbeit. Natürlich ist es nicht sinnvoll in PHP streng funktional zu programmieren und keine Schleifen und Variablen mehr zu verwenden. Auch im Beispiel der Funktion array_map ist ersichtlich, dass der rein funktionale Code nicht unbedingt einfacher und verständlicher wird. Funktionale Sprachen sind dagegen für das funktionale Paradigma optimiert, wie das folgende Beispiel derselben Funktion array_map in der funktionalen Programmiersprache OCaml zeigt.
let rec array_map my_fun = function
| [ ] -> [ ]
| x::xs -> [my_fun x] @ array_map my_fun xs;;
OCaml Code Erklärung: Die | Zeilen dienen hierbei als Vergleichsstruktur (Pattern Matching) und funktionieren ähnlich wie ein switch Befehl. Ist das übergebene Array (bzw. in diesem Fall eine Liste) leer, entspricht also [ ], wird eine leere Liste zurückgegeben. Sind noch Elemente in der Liste, wird diese in das erste Element (x) und eine Restliste (xs) aufgetrennt. Auf das erste Element wird die übergebene Funktion my_fun angewandt und die Funktion array_map rekursiv mit der Restliste aufgerufen. Das Symbol @ verbindet zwei Listen und führt in dem Beispiel die resultierenden Listen der rekursiven Aufrufe zusammen. Das Ergebnis ist eine Liste, in der auf alle ursprünglichen Elemente die übergebene Funktion my_fun angewandt wurde.
Was bringts?
Bisher waren funktionale Programmiersprachen vor allem in der akademischen Welt anzutreffen, da sie der Mathematik sehr nahe stehen und daher im wissenschaftlichen Bereich oft Anwendung finden. Klassische Vertreter sind Haskell, Erlang, OCaml, Scala, Scheme, Lisp, aber auch moderne Sprachen wie zum Beispiel PHP, integrieren Features der funktionalen Welt. Einen wichtigen Beitrag zur Verbreitung hat sicher auch JavaScript geleistet, da hier stark auf funktionale Sprachkonstrukte zurückgegriffen wird.
Doch welche Vorteile ergeben sich durch die Verwendung von funktionalen Sprachen bzw. ihren Features?
Die Verwendung von Funktionen als Parameter (Higher-Order Functions) ist eine der wichtigsten Funktionalitäten, die einem das Programmierleben stark vereinfachen können. Sie sind meist wesentlich flexibler und können daher öfters im Programmcode wiederverwendet werden. Zusätzlich ist es möglich, komplexe Abläufe zu abstrahieren und dann in beliebigen Bereichen wiederzuverwenden. Dank PHP 5.3 ist das nun auch für den PHP Programmierer möglich.
Das funktionale Konzept ist auch keineswegs nur ein akademisches Hirngespinst, sondern ist seit langer Zeit auch im produktiven Einsatz (z.B. Anwendungen in der Telekommunikation mit Erlang) anzutreffen.
Ein weiterer Vorteil, der vor allem durch die starke Zunahme der Parallelrechner (Quad-Core ist bereits im Consumer-Bereich der Standard) immer wichtiger wird, ist die einfache Möglichkeit zur Parallelisierung. Da in streng funktionalen Programmiersprachen keine Seiteneffekte auftreten können, kann eine Funktion unabhängig vom restlichen Programm zum Beispiel auf einem anderen Prozessor oder Rechner ausgeführt werden. Auch Google bedient sich mit dem MapReduce Konzept (beschrieben im phphatesme Artikel …und das Leben nach SQL geht weiter … jetzt wird reduziert!) diesem Vorteil und vereinfacht so die Verteilung von Tasks (z.B. Suchindexerzeugung) über mehrere tausend Server hinweg.
Auch die laufende Integration von funktionalen Sprachfeatures in aktuellen modernen Programmiersprachen zeigt zusätzlich, dass es sich hierbei um ein gelungenes Konzept handelt, das die Programmierung stark vereinfachen kann. Eine tiefere Auseinandersetzung mit diesem Thema ist also auch für den klassischen PHP Programmierer sinnvoll und erweitert nicht nur den imperativen Tellerrand, sondern ermöglicht auch eine Verbesserung des Programmierstils bzw. Ausnützung modernen Sprachfeatures in PHP. PHPhatesme plant ebenfalls weitere Beiträge zu diesem Thema – über Anregungen und Input freuen wir uns natürlich auch in den Kommentaren.
Hier wie immer noch einige weiterführende Links: