toscho.design

PHP: Code schönfinkeln (Currying)

Quelle: Wikipedia

Moses Ilyich Schönfinkel, russisch: Моисей Исаевич Шейнфинкель, (1889—1942) war ein russischer Logiker und Mathematiker. Er hat die Kombinatorische Logik erfunden, mit der Variablen aus der mathematischen Logik herausgehalten werden sollten.

Von 1914 bis 1924 hat er in Göttingen gearbeitet, danach zog er nach Moskau. Wenn man der Wikipedia glauben möchte – die Quellen konnte ich nicht mehr alle finden – so wurde er später geisteskrank und lebte in Armut bis zu seinem Tod 1942. Seine Nachbarn haben seine Notizen zum Heizen verbrannt.

Heute soll uns ein Verfahren interessieren, das meistens Currying genannt wird, nach Haskell Brooks Curry, der es zwar auch publiziert hat – aber eben später als Schönfinkel. Dabei geht es nicht um das Abschaffen von Variablen, wohl aber um deren Reduktion. Und das ist immer noch bemerkenswert praktisch.

Nehmen wir eine Funktion, die zwei Variablen erwartet:

function add( $x, $y )
{
    return $x + $y;
}
print add( 10, 5 ); // 15

Sterbenslangweilig, ich weiß. Jetzt denken wir uns eine Situation, in der wir erst add( 10, 5 ) brauchen, dann add( 10, 10 ), dann add( 10, 3456 )
Das können wir alles einfach so hinschreiben. Kein Problem. Aber was tun wir, wenn wir irgendwann feststellen: Mist, wir brauchen nicht 10 als ersten Wert, sondern 11?

Zwar bietet unser Editor das Suchen und Ersetzen an, aber das ist fehleranfällig und schlimmer noch: wenig elegant.

Stattdessen schönfinkeln wir unseren Code: Wir bauen add() so um, daß es nur eine Variable erwartet. Damit klarer wird, was sie tut, habe ich sie leicht umbenannt.

function add_to( $x )
{
    return function( $y ) use ( $x )
    {
        return $x + $y;
    };
}

Die Funktion erwartet immer noch $x als ersten Parameter. Dann liefert sie aber nicht das Ergebnis einer Addition zurück (wie auch?), sondern eine neue, anonyme Funktion, auch Closure genannt, die sich $x merkt und $y als Parameter empfängt.
Und so benutzen wir das:

$add_to = add_to( 10 );
print $add_to( 5 ); // 15
print $add_to( 10 ); // 20
print $add_to( 3456 ); // 3466

Wenn wir jetzt die Basiszahl ändern wollen, fassen wir nur noch die erste Zeile an und ändern die 10 zu einer 11.

Nun sind Beispiele mit mathematischen Grundoperationen zwar hübsch übersichtlich – aber wenig praxisrelevant. Suchen wir uns etwas, das wir tatsächlich benutzen können.

Wir wollen PHP-Klassen laden, die wir in Unterverzeichnissen abgelegt haben. Das könnte so aussehen:

require_once './inc/class.Tokenizer.php';
require_once './inc/class.PHP_Tokenizer.php';
require_once './inc/class.Lexer.php';
require_once './inc/class.PHP_Lexer.php';
require_once './inc/class.Highlighter.php';
require_once './inc/class.PHP_Highlighter.php';

Hier haben wir zwei Probleme: inc ist ein dämlicher Name für ein Verzeichnis, und wir wiederholen uns zu oft.

Lösen wir zunächst das erste Problem: Wir packen die Tokenizer und Lexer in das Verzeichnis models und die Highlighter in views. Jetzt sieht man der Verzeichnisstruktur schon an, was der Code darin tut. Prima.

Für das zweite Problem packen wir alles, was wiederholt wird, in einen String mit Platzhaltern und lassen sprintf() die restliche Arbeit erledigen:

require_once sprintf( 
    './%1$s/class.%2$s%3$s.php', 
    'models', 
    'PHP_', 
    'Lexer' 
);

… würde dann ./models/class.PHP_Lexer.php laden. Wir sind aber geizig und wollen auch diesen Aufruf abkürzen, damit wir das require_once später wiederum nur an einer Stelle zu beispielsweise include ändern können. Und diese eine Stelle sei unsere Funktion:

function load_pattern( $pattern )
{
    return function() use ( $pattern )
    {
        $args = array_merge( array ( $pattern ), func_get_args() );
        $path = call_user_func_array( 'sprintf', $args );
        require_once $path;
    };
}

Und jetzt haben wir endlich einigermaßen flexiblen Code:

$load = load_pattern( './%1$s/class.%2$s%3$s.php' );
$load( 'models', '',     'Tokenizer' );
$load( 'models', 'PHP_', 'Lexer' );
$load( 'views',  'PHP_', 'Highlighter' );

Auch dieser Code ließe sich so reduzieren, daß wir jeweils nur einen Parameter übergeben – Closures können schließlich selber wieder Closures zurückgeben. Das fände ich allerdings schwer zu lesen – und Lesbarkeit ist neben der Prägnanz eben auch ein wichtiger Baustein gut wartbaren Codes.

Wir könnten statt der hier gezeigten Beispiele auch eine gewöhnliche Klasse und __invoke() benutzen. Damit man sich aber für oder gegen etwas entscheiden kann, muß man die Alternativen kennen, finde ich.

Der Anlass für diesen Artikel war ein Tweet Thomas Meinikes. Danke dafür.

7 Kommentare

  1. Ralf Albert am 19.08.2012 · 01:13

    Eine andere Alternative wäre der Einsatz der Standard-Klasse stdClass. Für mich sogar etwas logischer, da z.B. im a+b-Beispiel a eine statische Variable ist.
    Der Nachteil an deiner Lösung ist der, dass man im Nachhinein nicht mehr feststellen kann welchen Wert a hat (var_dump( $add_to ); // object(Closure)

    Ein etwas nicht so ganz sinnvolles aber praxisnahe Beispiel wäre vorhandene Objekte zu erweitern:

    
    global $post;
    $post->post_link = function() use ( $post ){
    	return sprintf(
    		'<a href="%s" title="%s">%s</a>',
    		$post->guid, $post->post_name, $post->post_title
    	);
    };
    $postlink = $post->post_link;
    echo $postlink();
    
  2. Ralf Albert am 19.08.2012 · 01:16

    OK, den Code hat es im Kommentar zerhauen. Hier als pastebin.

  3. Thomas Scholz am 19.08.2012 · 01:42

    @Ralf Albert: Den Wert von $a bekommst du ganz einfach mit $add_to( 0 ). Für eine eine echte Inspektion braucht man vermutlich die Reflection-API. Allerdings würde ich diese Konstruktion auf Fälle beschränken, die nicht so komplex werden, daß man später eine Untersuchung braucht.

    Deinen Code habe ich repariert. Bitte nimm nie die GUID als Link-Adresse. Sie ist keine, nach einem Umzug auf eine andere Domain ändert sie sich nicht. Ich nähme in diesem Falle einfach get_permalink().

  4. Ralf Albert am 20.08.2012 · 05:44

    @Thomas Scholz: Möchte man testbaren Code schreiben, kommt man nicht drum rum Konstrukte zu verwenden die man untersuchen kann. Auch beim Debuggen und Profiling macht man sich damit das Leben wahrscheinlich unnötig schwer.

    OOP setzt ja im Prinzip genau das um, was du in deinem Artikel beschrieben hast. Anstatt einer Funktion etliche Parameter zu übergeben von denen sich dann auch nur wenige verändern, erzeugt man Objekte die als Eigenschaft eben jene unveränderten Parameter haben.
    Oft habe ich das Gefühl das viele bei OOP an komplizierte Klassen, Sichtbarkeiten von Methoden und Eigenschaften, Dependency Injection und solche Sachen denken. OOP bedeutet (für mich) aber vielmehr wie man mit seinen Variablen umgeht. Anstatt sie "lose" im Code zu verwalten, verwaltet man sie in Objekten. So gesehen ist Currying/Schönfinkeln in OOP quasi eingebaut.

  5. Thomas Scholz am 20.08.2012 · 06:05

    @Ralf Albert: Anonyme Funktionen sind ja Instanzen der Klasse Closure – und so gesehen … Quasi-OOP.

    Beim Testen möchte ich gerade nicht wissen, wie eine Funktion intern arbeitet, sondern ob sie das vereinbarte Ergebnis liefert. Die Interna sind egal, solange alle Tests bestanden werden.

  6. Ralf Albert am 21.08.2012 · 00:59

    Klassen != OOP. Genauer gesagt: Nur weil man Klassen benutzt, programmiert man nicht gleich objektorientiert.
    $a = array();
    $a['y'] = 10;
    $a['add_to'] = function( $x ) use( $a ){ return $x + $a['y']; };

    Lassen wir mal außer Acht das Closures PHP-intern als Klasse gehandhabt werden, wäre das auch schon ein Objekt. php.net sagt zu Closures ohnehin: Anonymous functions are currently implemented using the Closure class. This is an implementation detail and should not be relied upon.

    WIE eine Funktion an Wert X heran kommt, interessiert beim Testen nicht wirklich. Man möchte beim Testen aber verlässlich sagen können welchen Wert X hat.
    Wenn man aber eben jene Funktion aufrufen muss die man testen will um an den Wert von X heran zu kommen, dann ist der Test wenig aussagekräftig. Die Funktion würde sich selber bescheinigen das sie einwandfrei funktioniert. Die Funktion ist nicht überprüfbar da es keine Revision gibt die man prüfen kann.

    Bei so einfachen Konstrukten kann man das vernachlässigen. Wenn es etwas komplizierter wird, bekommt man aber schnell Probleme.

  7. Hasenplautze am 12.09.2012 · 23:46

    Ich weis jetzt nicht genau, auf was der Beitrag abzielt - aber wenn ich den Quelltext lese, dann fällt mir nur eins ein: Magic!

    Magic ist schlecht! Schlecht lesbar, schlecht wartbar, schlecht entwickelbar!

    Für solche Fälle wie im letzten Quelltext benutzt man Autoloader - ggf. schreibt man dafür eine Klasse, die das sauberer und transparenter handelt.