JavaScripts bind() für Perl

Die Methode bind() von JavaScript existiert in Perl nicht. Aber Perl wäre nicht Perl, ließe sich ein eine äquivalente Funktionalität nicht auf eine völlig transparente und idiomatische Art und Weise mit nur wenigen Zeilen Code realisieren.

In einem früheren Blog-Post habe ich bereits --- sowohl für JavaScript als auch Perl --- beschrieben, wie Closures als Kitt, als Glue-Code verwendet werden können, sollten ein Callback-API und eine Callback-Funktion nicht wie gewünscht zusammenpassen. Die Closure übersetzt in diesem Fall die Funktionssignaturen. Die gleiche Technik lässt sich auch einem prozeduralen Callback-API unterjubeln, wenn als Callback eine Instanzmethode eines Objektes statt einer einfachen Funktion aufgerufen werden soll.

Die Perl-Lösung sah folgendermaßen aus:

#! /usr/bin/env perl

package Logger;

use strict;

sub new {
    my ($class, $prefix) = @_;

    bless \$prefix, $class;
}

sub logMessage {
    my ($self, $msg) = @_;

    my $now = localtime;
    print STDERR "[$now][$$self]: $msg\n";
}

package main;

use strict;

sub run_service {
    my ($logger) = @_;

    $logger->("starting service");

    # Do something.

    $logger->("stopping service");
}

foreach my $service ("cruncher", "crasher") {
    my $prefix = $service;
    
    my $logger = Logger->new($prefix);
    
    my $callback = sub {
        my ($msg) = @_;

        $logger->logMessage($msg);
    };

    run_service $callback;
}

Die Funktion run_service() in Zeile 24 steht für einen Dummy-Microservice, der als Argument eine Log-Funktion als Callback übergeben bekommt. Und selbiger Callback wird ebenfalls mit lediglich einem Argument, der zu protokollierenden Meldung aufgerufen.

Wir wollen dagegen, ein Logger-Objekt verwenden, und seine Methode logMessage() aufrufen lassen. Die Lösung (Zeilen 39-45) besteht darin, eine Closure als Callback-Funktion zu übergeben, und das Logger-Objekt in den Aufrufkontext zu injizieren.

So weit, so gut. Aber JavaScript hat auch noch die Methode bind() auf Lager, die eine idiomatischere Lösung erlaubt:

runService(logger.logMessage.bind(logger));

Die Methode bind() erzeugt eine neue Funktion, bei der das Schlüsselwort this den Wert repräsentiert, der als erstes Argument übergeben wurde. Die weiteren Argumente werden einfach an die Funktion durchgereicht, gefolgt von den Argumenten, die beim Aufruf der neuerzeugten Funktion übergeben wurde.

Die gleiche Funktionalität lässt sich in Perl auf lächerlich einfache Art und Weise abbilden:

package Logger;

use strict;

sub new {
    my ($class, $prefix) = @_;

    bless \$prefix, $class;
}

sub logMessage {
    my ($self, $msg) = @_;

    my $now = localtime;
    print STDERR "[$now][$$self]: $msg\n";
}

package main;

use strict;

sub run_service {
    my ($logger) = @_;

    $logger->("starting service");

    # Do something.

    $logger->("stopping service");
}

foreach my $service ("cruncher", "crasher") {
    my $prefix = $service;
    
    my $logger = Logger->new($prefix);
    
    run_service(Logger->bind(logMessage => $logger));
}

package UNIVERSAL;

sub bind {
    my ($self, $method, $this, @args) = @_;

    my $coderef = $self->can($method);

    return sub {
        my (@more_args) = @_;

        return $coderef->($this, @args, @more_args);
    }
}

Die erste Zeile, die sich gebenüber der vorherigen Version geändert hat, ist Zeile 37:

run_service(Logger->bind(logMessage => $logger));

Dies ist lediglich eine idiomatische Übersetzung der JavaScript-Version nach Perl:

runService(logger.logMessage.bind(logger));

Aber wie kommt es, dass bind() plötzlich auf magische Art für unser Logger-Objekt definiert ist? Beginnend in Zeile 40, wird die Klasse UNIVERSAL --- die Mutter aller Perl-Klassen --- durch eine neue Methode bind() erweitert.

Ihr erstes Argument ist der Name der Methode, die an ein anderes Objekt gebunden werden soll. In Zeile 45 wird eine Referenz auf die Methode erzeugt (wer das nicht versteht, sollte die Dokumentation von UNIVERSAL->can in perldoc UNIVERSAL lesen). Und übrigens kann diese Perl-Version von bind() sowohl als Klassen- auch als als Instanzmethode aufgerufen werden, weil UNIVERSAL->can sowohl als Klassen- auch als als Instanzmethode aufgerufen werden kann. Und der entsprechende Code kann in jede beliebige Moduldatei geschrieben werden. Sie sollte nur nicht UNIVERSAL.pm genannt werden.

Der Rest ist mit Grundwissen über Closures einfach zu verstehen:res:

Unser Perl-bind() erzeugt --- genau wie das JavaScript-Pendant - eine neue Funktion, eine Closure, und gibt sie zurück, wobei allerdings die Instanz --- nämlich $self in Perl oder this in JavaScript --- durch den Wert ersetzt wurde, der bind() als erstes Argument übergeben wurde. Weitere Argumente werden einfach angehangen, gefolgt von denen, mit denen die neue Funktion aufgerufen wurde.

Das ist allerdings nicht zu 100 % die gleiche Funktionalität, die bind() in JavaScript hat. In JavaScript ist jede Funktion auch ein Objekt, und es können somit Methoden auf sie aufgerufen werden. In Perl dagegen ist eine Funktion zunächst einmal lediglich eine Referenz. Um Methoden auf sie aufzurufen, muss sie zuerst gebless()t werden. Allerdings ist this in JavaScript ein Schlüsselwort, und das Perl-Äquivalent $self ist lediglich der konventionelle Name für das erste Argument und kann durch jeden beliebigen anderen Namen ersetzt werden.

Dementsprechend wäre es wenig sinnvoll, bind() auf reguläre Perlfunktionen anzuwenden. Genaugenommen ist unser bind() für Perl wohl ohnehin eher Code-Golf denn eine sinnvolle Erweiterung. Wer eine sinnvolle Anwendung gefunden hat, kann gerne einen Kommentar hinterlassen.


blog comments powered by Disqus