AngularJS - Vorhandene Services erweitern

Wer im Backend viel mit Scala zu tun hat und dann im Frontend mit JavaScript und AngularJS konfrontiert ist, vermisst schon mal die eine oder andere Funktion aus Scalas umfangreicher Standardbibliothek1. Ein Beispiel dafür ist die Methode Future.traverse2 aus dem concurrent-Paket von Scala 2.10, die wir auch gerne in JavaScript bzw. AngularJS verwenden möchten.

traverse erlaubt es, eine Funktion auf eine Liste von Werten anzuwenden. Die Funktion muss dabei aus jedem Wert der Liste eine Future machen. Stark vereinfachte Signatur:

def traverse[A,B](in: Traversable[A])(fn: (A) => Future[B]): Future[Traversable[B]]

Der Witz dabei ist, dass man nicht ein Traversable mit Futures bekommt (sonst könnte man auch map verwenden), sondern ein Future mit einem Traversable der angepassten Werte. Diese Traversable kann man dann mit den üblichen Funktionen innerhalb der Future weiterverarbeiten.

Traversieren einer Seq

In JavaScript gibt es als Pendant zu den Scala Futures die Promises aus der Bibliothek q. AngularJS bietet einen Service namens $q, der aus Platzspargründen nur eine kleine Untermenge von q abbildet.

Auf eine Stackoverflow-Frage3 hin inspiriert möchte ich zeigen, wie man die traverse Funktion aus Scala in JavaScript implementieren und außerdem dem $q-Service in AngularJS hinzufügen kann, um sie folgendermaßen aufzurufen:

$q.traverse([1,2,3], function(i){...; return promise;})

Decorator

Ein Java- und C++-Programmierern altbekanntes Pattern heißt Decorator. Kurzgesagt umhüllt man bei diesem Ansatz eine Klasse/Objekt/Funktion mit einer anderen Klasse/Objekt/Funktion. Man dekoriert es mit neuer Funktionalität. In Java ist dieses Pattern weit verbreitet, da man Objekte zur Laufzeit nicht erweitern kann. In JavaScript unterliegt man dieser Einschränkung nicht, sogar die Vererbung ist in JavaScript über den Prototypen zur Laufzeit veränderbar.

Da die AngularJS-Services nicht einheitlich erstellt werden (je nachdem ob man provide, service, factory, etc. nutzt), bietet sich das Manipulieren des Prototypen nicht an. Das Decorator-Pattern funktioniert jedoch auch unter diesen Umständen. Theoretisch kann man einen Service im Controller deokieren, aber natürlich wollen wir dies an einer zentralen Stelle tun, damit die neuen Funktionen der gesamten Anwendung zur Verfügung stehen.

In AngularJS wird jeder Service über $provide bereitgestellt, also ist das logischerweise auch der Ort, an dem man Services dekorieren kann. $provide bietet dafür eine Funktion namens decorator(name, fn), die als Parameter den Namen des zu dekorierenden Services und die umhüllende Funktion erwartet. Die Funktion muss einen Parameter namens $delegate annehmen und auch wieder zurück geben, damit das ganze Konstrukt funktioniert. Definiert wird es in der Konfigurationsphase eines Moduls, und aufgerufen wenn $provide denn Service injiziert. Man kann nun $delegate erweitern, mit einem eigenen Objekt umhüllen, oder auch ein völlig neues Objekt zurückgeben.

angular.module("app", []).config(function($provider) {
  $provider.decorator("$q", ["$delegate", function($delegate) {
    $delegate.traverse = ...;
    return $delegate;
  }])
});

Traverse

Nun zur eigentlichen Funktion. Eine erste einfache Implementierung könnte so aussehen:

var traverse = function(values, fn) {
  var i, promise;
  var promises = [];
  for (i = 0; i < values.length; i++) {
    promise = fn(values[i]);
    promises.push[promise];
  }
  return $q.all(promises);
}

Wir iterieren also über den Array, führen für jeden Wert die übergebene Funktion aus (die für den Eingabewert eine Promise erzeugen muss; ohne Typsicherheit müssen wir uns darauf verlassen, dass der Nutzer diesem Vertrag auch folgt), und schließlich übergeben wir alle so erzeugten Promises der all()-Funktion von $q, die nun dafür sorgt, dass die Promises ausgeführt werden, bevor eine nächste eventuelle then-Funktion aufgerufen wird.

var addOne = function(i) {
  var deferred = $q.defer();
  // Komplizierte Berechnung oder Netzwerkaufruf
  // Dauert 1 Sek.
  $timeout(function() {
    deferred.resolve(i + 1);
  }, 1000);
  // Wird schon vor Ende der Berechnung zurück gegeben
  return deferred.promise;
};

traverse([1,2,3], addOne).then(function(results) {
  // results = Array mit den berechneten Werten
  console.log(results);
  // => [2,3,4]
});

Damit hätten wir nun die Scala-Version abgebildet. Man könnte das Ganze noch umdrehen, um so traverse vorzukonfigurieren und nur noch die jeweiligen Werte übergeben zu müssen.

var adder = $q.traverse(addOne)
...
adder([1,2,3])
// => $q([2,3,4])
adder([5,6,7])
// => $q([6,7,8])

Dazu brauchen wir also eine geschönfinkelte curried4 Funktion, d.h. eine Funktion, die statt mehrerer Argumente einzelne Argumente erwartet, und solange neue Funktionen mit einem Parameter zurückliefert, bis die ursprüngliche Funktion erreicht ist. Dann könnten wir den ersten Wert vorbelegen (und hätten damit eine partiell angewendete Funktion). Dieses Feature wird zwar von JavaScript nicht direkt unterstützt, aber durch das Schachteln von Funktionen kann der gleiche Effekt erzeugt werden.5

var traverse = function(fn) {
  return function(values) {
    var i, promise;
    var promises = [];
    for (i = 0; i < values.length; i++) {
      promise = fn(values[i]);
      promises.push[promise];
    }
    return $q.all(promises);
  }
}

Eine letzte kleine Optimierung kann man sich aber noch vorstellen. Bei $q.all() passiert es mir gerne, dass ich vergesse, die Promises in einem Array zu schachteln, da man üblicherweise seine Promises hintereinander weg konstruiert. Bei traverse ist das nicht so wahrscheinlich, aber wir wollen dem Nutzer eine komfortable API zur Verfügung stellen.

Um das zu erreichen, definiert die geschachelte Funktion keinen Parameter mehr, sondern untersucht die Parameter zur Laufzeit über das JavaScript-Konstrukt arguments.

var traverse = function(fn) {
  return function(/*values*/) {
    var values;
    if (angular.isArray(arguments[0]))
      values = arguments[0];
    else
      values = arguments;
    var i, promise;
    var promises = [];
    for (i = 0; i < values.length; i++) {
      promise = fn(values[i]);
      promises.push[promise];
    }
    return $q.all(promises);
  }
}

Durch die Fallunterscheidung, ob es sich hier um einen Array oder um mehrere einzelne Parameter handelt, bieten wir eine praktisch zu nutzende Funktion.

var adder = $q.traverse(addOne)
...
adder([1,2,3])
// => $q([2,3,4])
adder(5,6,7)
// => $q([6,7,8])

Alles zusammen

Fertig sieht das Ganze nun so aus:

Den Code findet man auch hier.

  1. Das Re-Implementieren von Scala-Funktionalität in JavaScript wird sich in hoffentlich nicht allzu ferner Zukunft mit ScalaJS erledigt haben.

  2. In Scala 2.10 wurde scala.concurrent komplett überarbeitet und dabei u.a. die Futures aus Akka und Finagle übernommen; s. Future API

  3. http://stackoverflow.com/questions/18004360/traversing-promises-in-q/18031677

  4. http://de.wikipedia.org/wiki/Currying

  5. Für traverse sieht das Schachteln von Funktionen noch relativ harmlos aus, bei größeren Funktionen baut man sich dann Hilfsfunktionen. Siehe dazu auch Functional JavaScript

Diesen Post teilen

RSS-Feed

Neue Posts direkt im Newsreader.

RSS-Feed abonnieren

Newsletter

Neue Posts sowie Neuigkeiten rund um Big Data, Spark, und Scala. Maximal eine E-Mail im Monat.

Kommentare