Verbindung verschiedener Verabeitungsschritte

Häufig beinhaltet eine Software verschiedene Verarbeitungsschritte, die von den Daten nacheinander durchlaufen werden.

Im einfachsten Fall kann man mit Verabeitungsschritten f, g und h man so etwas machen wie (Pseudocode):


for x in input_data {
y = h(g(f(x));
  store_partial_result(y);
}

Das stimmt, wenn die Datenmengen jeweils übereinstimmen. Häufig ist aber in der Praxis der Fall, dass die Datengrößen nicht so übereinstimmen. Man muss also Daten zwischenspeichern, bis für den jeweiligen Verarbeitungsschritt genug Eingaben da sind. Im Grunde genommen ist ein sehr einfacher und robuster Mechanismuns seit Jahrhunderten bewährt, zumindest wenn man in Jahrhunderten von zehn Jahren Dauer rechnet.. 😉


cat input_data_file| process_f | process_g | process_h > output_data_file

Der Ansatz lässt sich in einem Programm direkt nachbauen. Man muss named oder anonymous Pipes, TCP/IP, Message-Queues oder Messaging (wie JMS, RabbitMQ, MQ-Series,…) verwenden, um die Programmteile miteinander zu verbinden. Wenn man mit Akka arbeitet, kann man für jeden Verarbeitungsschritt einen (oder mehrere parallele) Aktor verwenden und diese mit Messages verknüpfen. Die Aktoren müssen und Daten sammeln, bis sie genug haben, um sie sinnvoll zu verarbeiten. Etwas unschön ist, dass man hier plötzlich „state“ im Aktor braucht.

Mehrere Threads sind auch möglich, wenn man für die Daten jeweils den augenblicklichen „Owner“ kennt und nur diesem Zugriff darauf erlaubt. Die zu implementierende Logik passt eigentlich sehr gut dazu, da definiert sein sollte, welchem Verarbeitungsschritt die Daten gerade unterzogen werden.

Es lässt sich aber auch mit einem Thread etwas machen, etwa entlang dieser Linie:

while (true) {
  if (enough_for_h(g_out)) {
    h_in = get_block(g_out, required_size_for_h)
    h_out = h(h_in)
    store_result(h_out)
  } else if (enough_for_g(f_out)) {
    g_in = get_block(f_out, required_size_for_g)
    append(g_out, g(g_in));
  } else if (enough_for_f(new_data)) {
    f_in = get_block(new_data, required_size_for_f)
    append(f_out, f(f_in));
  } else {
    append(new_data, read_block(input_data));
  }
}

Man muss immer aufpassen, dass sich das System nicht totläuft, was bei schlechten Implementierungen leicht passieren kann, weil entweder die Eingabedaten ausgehen oder die Buffer überlaufen. Die obige Implementierung strebt an, die Buffer immer auf der minimalen Füllung zu belassen.

Oft hat man natürlich die Möglichkeit und auch die Notwendigkeit, alle Daten auf einmal ins Memory zu laden. Dann gehen viele Dinge einfacher, man denke nur an Sortierfunktionen, die ja alle Daten sehen müssen.

Für große Datenmengen ist das aber nicht möglich und da sind die richtigen Ansätze nützlich, um nur eine verträgliche Datenmenge im Speicher zu halten.

Ein recht radikaler Ansatz ist es, alle Zwischenergebnisse in Dateien oder Datenbanken zu speichern. Dann kann man die Schritte sogar nacheinander ausführen.

In jedem Fall sollte man sich über die richtige Architektur Gedanken machen, bevor man munter drauf los programmiert.

In der Regel fährt man am besten, wenn die Quelldaten den Prozess treiben, also wenn diese sequentiell gelesen werden und zwar jeweils in den Mengen, die auch verabeitet werden. Das ist das Prinzip des „Backpressure“.

Share Button

Non-Blocking I/O

In Posix-Systemen (Linux, Unix, MacOS X,…) basieren die I/O-Operationen hauptsächlich auf den Systemaufrufen read(..) und write(..). Die meisten anderen I/O-Operationen lassen sich darauf zurückführen und auch I/O von anderen Programmiersprachen als C dürfte letztlich indirekt zu read() und write() führen. read() ist eine Funktion, die einen (numerischen) Filedeskriptor, einen Pointer auf einen hinreichend großen Speicherbereich („Buffer“) und eine Größenangabe annimmt und im Idealfall die angegebene Anzahl von Bytes liest. Das funktioniert gemäß dem Unix-Prinzip „everything is a file“ ganz einfach mit Dateien (Files), Geräten (Devices), (Pseudo-)terminals, Pipes (Fifos), TCP-Streams etc. Wenn ein Fehler auftritt, wird ein negativer Wert zurückgegeben und man muss errno abfragen, um den Fehler zu finden. Den negativen Fehlercode direkt zurückzugeben wäre besser gewesen, aber das kann man nun nicht mehr ändern. Eine 0 bedeutet, dass man das Ende erreicht hat. Dies wird nicht durch den Inhalt der übertragenen Daten ausgedrückt, denn jede beliebige Bytesquenz wird akzeptiert und verarbeitet, sondern separat übermittel. Wenn weniger als die gewünschten Bytes da sind, dann wird entsprechend weniger zurückgegeben. Der Rückgabewert gibt an, wieviele Bytes tatsächlich gelesen wurdn. Das sind alles Situationen, in denen das read sofort anfangen kann, auch wenn die eigentliche Operation etwas länger dauern kann. Bei Dateien kann der Fall, dass 0 Bytes gelesen werden, nur am Dateiende und natürlich bei Fehlern auftreten. Bei Pipes und TCP-Streams ist aber möglich, dass man auf Daten wartet und keine da sind. Mit 0 Bytes gibt sich das read normalerweise nicht zufrieden und wwartet stattdessen auf Daten. Das ist eine vernünftige Sache, da es in der Regel das ist, was man will. So vereinfacht sich die Programmierung. Write verhält sich entsprechend, bei Pipes kann man auch nur schreiben, wenn auf der Gegenseite die Daten abgenommen werden. Ein Prinzip das heute in der Scala- und Akka-Welt als die größte Innovation des Jahres gerühmt wird und den Namen „Backpressure“ bekommen hat.

Non-Blocking funktioniert auch mit read(). Mit open() oder nachträglich mit fcntl() wird eingestellt, dass das I/O non-blocking ist. read() versucht nun, Bytes zu lesen und wenn das klappt ist alles wie beim blocking I/O. Wenn das read() keine Daten findet, wartet es nicht auf Daten, sondern kommt sofort mit -1 zurück und in errno steht EAGAIN oder EWOULDBLOCK o.ä. write() entsprechend.

Nun hätte man die Möglichkeit, regelmäßig einen Filedescriptor abzufragen und wenn Daten kommen, darauf zu reagieren und sonst etwas anderes zu tun. Ohne Multithreading…

Interessant ist der Fall, dass man mehrere Filedescriptoren gleichzeitig hat und von diesen Daten lesen oder schreiben will. Man weiß nicht wann welche Zugriffe möglich sind. Das wäre mit einem Thread pro Filedescriptor machbar. Oder mit non-Blocking-I/O und Polling, also einer Schleife, die dauernd alle Filedescriptoren durchprobiert.. Besser ist es aber select() oder pselect() oder poll() zu verwenden, die semantisch sehr ähnlich funktionieren, aber aus historischen Gründen koexistieren. Wer eigenen Code schreibt, kann sich eine der drei Funktionen aussuchen und damit arbeiten, wer aber Code von anderen Leuten lesen und ändern muss, sollte alle drei kennen. Dies ist eine sehr elegante Lösung, weil man noch ein Timeout dazunehmen kann. Die Spezialfälle 0 und unendlich für das Timeout werden auch sinnvoll unterstützt.

Das „non-Blocking-Prinzip“ wird bei der Posix-Systemprogrammierung oft angeboten, z.B. beim Locken von Mutexen, Semaphoren oder beim File-Locking.

Man sollte non-blocking-I/O nicht mit assynchronem I/O verwechseln. Dazu kommt vielleicht ein anderer Artikel..

Share Button

MoSQL

Bei einem Meetup-Treffen in Zürich wurde ein neues Backend für mysql-Datenbanken vorgestellt, das die Skalierung erleichtern soll:
MoSQL
Zur Zeit wird daran noch entwickelt, aber man kann Vorabversionen schon testen.

Nun stellt sich die Frage, ob man so etwas braucht, weil die meisten mysql-Installationen ja klein sind und für größere Datenbanken
PostgreSQL, Oracle, DB2 etc. existieren.
Dem kann man entgegensetzen, dass die wenigen großen mysql-Installationen eine besondere Bedeutung haben und deren Betreiber auch oft bereit sind, etwas zu investieren. Der Wechsel des Datenbankprodukts ist in der Praxis oft schwierig, deshalb bleibt man in der Regel bei dem DB-Produkt, mit dem man angefangen hat. Ein Argument am Anfang genau nachzudenken. Dummerweise werden kommerzielle DB-Systeme für richtig große Installationen oft richtig teuer. Nur die Einstiegsversionen sind günstig geworden.

Die andere Frage ist, warum man nicht auf die NoSQL-Schiene setzt. Das hängt vom Problem ab. Oft kann man NoSQL und SQL gut kombinieren für verschiedene Aufgaben. SQ_L als Abfragesprache ist recht mächtig und weit verbreitet und hat deshalb einen gewissen Reiz. NoSQL-Datenbanken haben oft „viel einfachere“ Abfragesprachen, wobei es Geschmackssache ist, ob die wirklich einfacher sind und wobei sich die Frage stellt, wie einfach komplizierte Dinge sind, die mit SQL zumindest möglich sind. Die andere Frage ist die Transaktionalität, die bei SQL-DB-Architektueren allerdings auch oft verwäsert wird, vor allem durch Caching. Gerade bei MySQL mit Clustering muss man hier aufpassen und auch bei MoSQL ist das ein Thema, wenn zueinander inkompatible Transaktionen parallel laufen. Durch die massive Parallelisierbarkeit kann so etwas passieren und dann muss eine Transaktion scheitern. Bei sequentieller Abarbeitung der Transaktionen (Modus SERIALZE) wäre das nicht passiert, aber man bekäme auch nicht die Performance. Umgekehrt können viele NoSQL-Datenbanken Transaktionen. Oft nur einfache oder Kombinationen aus zwei Statements, aber wer es wirklich wüst will kann die mit Two-Phase-Commit als „verteilte Transaktion“ kombinieren und hat dann sogennante „volle Transaktionalität“. Nur ist two-phase-commit nicht so wasserdicht wie ein lokales Commit….

Update 2019-03-23: Der Link http://dslab.inf.usi.ch/mosql/ zu dem erwähnten Projekt funktioniert nicht mehr und der Name MoSQL ist „recycelt“ worden für ein anderes Projekt, das MongoDB und PostgreSQL miteinander verbindet. Man kann also davon ausgehen, dass das ursprüngliche MoSQL-Projekt aufgegeben worden ist.

Share Button

Ist Ruby funktional

Wenn die Liste der funktionalen Sprachen erstellt wird, dann tauchen Haskell, Scala, Erlang, F#, Clojure und einige andere Lisp-Varianten auf.

Wenn man sich anschaut, welche Merkmale funktionale Sprachen auszeichnen, dann stellt sich die Frage, ob das nicht alles mit Ruby auch machbar ist.

Schauen wir einmal was man so typischerweise findet, meist auf Englisch:

  • Functions as „first class citizen“
  • Closures
  • Pure Functions
  • Higher Order functions
  • Everything returns a value
  • Immutability
  • No hidden state
  • Prefer recursion over iteration (immutable loop variable…)

Funktionen existieren in Ruby also losgelöste Objekte, in verschiedenen Formen, z.B. als proc, als lambda, als anonymer Block und durch Referenzierung einer Methode über ihren Namen (Reflection).

Closures werden von Ruby problemlos unterstützt, bei all diesen Formen der Funktionen werden Variablen aus dem definierenden Kontext eingebunden, wo sie referenziert werden. Methoden kann man übrigens als Spezialfall von Closures ansehen, weil sie als Kontext das Objekt einbinden, zu dem sie gehören.

Unter „pure function“ versteht man Funktionen, die den Funktionen aus der Mathematik entsprechen. Sie haben absolut keinen Seiteneffekt, sind reproduzierbar, können in beliebiger Zahl parallel ausgeführt werden und geben bei mehrfachen Aufrufen immer dasselbe Resultat. Sie eignen sich für Memoize, was nun eigentlich wieder ein Seiteneffekt ist, aber sozusagen ein transparenter. Man denke an sort() und sort!() in Ruby. Dies verlangt Disziplin vom Entwickler und gute Dokumentation.

„Higher Order Functions“ sind Funktionen höherer Ordnung, die also selbst Funktionen als Parameter oder Rückgabewert haben. Das ist in der funktionalen Programmierung Routine und nicht so ein spezieller Spezialfall, den man mal alle paar Jahre benutzt, wie Funktionspointer in C. Typische Beispiele sind Methoden wie inject(), map(), group_by()… each() sollte man nur verwenden, wenn man die Seiteneffekte wirklich braucht.

Alle Ausdrücke haben einen Wert. Das ist recht gut in Ruby umgesetzt, z.B. x = if (...) ... else ... end kann man verwenden…

Immutibility (Unveränderbarkeit) ist die Achillesferse. Es wird nicht wirklich gut unterstützt. Man soll Variablen nicht neu zuweisen und auch nicht irgendwas mutieren. Warum nimmt man nicht freeze()? Wir brauchen ein deepfreeze(), aber was bedeutet das? Wie sieht es mit collections aus? Es gibt immer Möglichkeiten, diese unverändert zu lassen und mit jedem Schritt eine neue Collection zu produzieren. Dasselbe gilt für Zeichenketten.

„No hidden State“, also kein versteckter Zustand. Man kann Kontextobjekte haben und herumreichen. Zustand ist unerwünscht und sollte kontrolliert an wenigen Orten gehandhabt werden.

„Recursion instead of Iteration“ steht für Rekursion statt Iteration. Funktionale Sprachen unterstützen Optimierung bei Tailrekursion (Endrekursion). Einige andere Sprachen, z.B. C mit gcc, Scala und viele Lisp-Dialekte, aber nicht Java, unterstützen diese Optimierung und erlauben es, zumindest Endrekursion ohne zu große Furcht einzusetzen. Bei Ruby hängt es von der Version und den Einstellungen ab, ist also mit Vorsicht zu genießen. Der Ruby-Ansatz ist es eher, die Iteratorn zu verwenden.

Fazit: Man kann mit ein paar Einschränkungen in Ruby funktional programmieren, aber es erfordert etwas mehr Disziplin, weil einige Dinge vom Entwickler beachtet werden müssen und nicht von der Sprache unterstützt werden.

Share Button

Zufällige Zeichenkette erzeugen

Oft braucht man so eine zufällige Zeichenkette, die nur aus bestimmten Zeichen bestehen darf.

Hier ist eine einfache Ruby-implementierung dafür:


#!/usr/bin/ruby

arr = ('a'...'z').to_a + ('A'...'Z').to_a + ('0'...'9').to_a + ['.', '/']
val = (0..16).inject("") do |a, x| i = (arr.size() * rand()).to_i;a + arr[i] end
puts val

Es wird eine 16-Zeichen lange Zeichenkette generiert, die aus den Zeichen [a-zA-Z0-9./] besteht.

Share Button

Jolla Mobiltelefon

Bekanntlich hat sich die Firma Nokia erst aus ihren eigenen Entwicklungen bezüglich Mobiltelefonsoftware und später auch aus dem zugehörigen Hardwaregeschäft zurückgezogen und erlaubt einer Nordamerikanischen Firma für eine gewisse Zeit, ihre Telefone mit „Nokia“ zu benennen, etwa so wie für die Autofans „Volvo“ auf Autos steht, die sicher nicht von der seit vielen Jahren auf ihr Kerngeschäft, nämlich Lkws, Baumaschinen und Busse, spezialisierten Firma Volvo hergestellt werden. Nun hat Nokia in den letzten Jahren viele gute Mitarbeiter verloren, die im Bereich der Mobiltelefonentwicklung tätig waren. Diese haben dann eine neue Firma, „Jolla“, gegründet. Bis hier ist das alles normal, oft gründen entlassene oder frustriert selber gegangene ehemalige Mitarbeiter eine Firma, mit der sie das machen wollen, was sie bei ihrem alten Arbeitgeber machen wollten, aber nicht durften. Manche von diesen Firmen sind dann sehr erfolgreich, aber gleich das ehemalige Kerngeschäft der einst größten und wichtigsten Firma Europas in so einer Startup-Firma neu aufzubauen ist schon recht ehrgeizig und man musste es immer mit einer gewissen Skepsis betrachten. Auch wenn Jolla dort eher ein Nischenanbieter und nicht mehr wie einst Nokia und danach bis heute Samsung Marktführer ist.

Nun ist aber Jolla seit einigen Monaten so weit und man kann die Telefone bestellen, bezahlen und mit ihnen sogar telefonieren. Ich habe mir so eines beschafft und die Grundfunktionen sind auch vorhanden und ganz brauchbar. Die Benutzeroberfläche ist etwas gewöhnungsbedürftig, was aber eher daran liegt, dass wir die Oberfläche von Android-Geräten gewohnt sind und hier einige Dinge im Detail etwas anders laufen. Typisch ist, dass es eine Art virtuellen Bildschirm gibt und dass man Applikationen verlässt, indem man vom rechten Rand kommend auf den Bildschirm streicht. Das ist besonders gut zu wissen für das „Tutorial“, das irgendwie immer wieder bei Starten des Gerätes hochkommt und für das man keine einfache Art findet, um es zu verlassen. Ein anderes häufiges Element sind Punktreihen oben links, die irgendwie eine Tiefe im Dialog angeben und mit denen man zurück zur nächst höheren Ebene kommt. Menüs bekommt man oft, indem man von oben in den Bildschirm hereinstreicht. Alles drei Dinge, die Android mit drei spezifischen Tasten gelöst hat.

Nun soll man Apps installieren können. Dafür sollen sowohl Android-Apps vom Google-Play-Store als auch native in C++ & Qt geschriebene Apps vom Jolla-Eigenen App-Store verfügbar sein. Mehr dazu kommt vielleicht, wenn ich damit Erfahrungen gesammelt habe. Da Jolla wie Android auf Linux basiert, wäre vielleicht ein xterm und die ganzen GNU-Tools und so etwas schön. Es sollte möglich sein. Für Android gibt es das auch, aber da muss man wohl alles in Java nochmal schreiben, um es in eine App zu bringen. Was ja passiert ist, man hat Apps mit den wichtigsten GNU-Tools oder zumindest gleichnamigen Java-Attrappen. Auf dem N900 war das alles einfach nativ dabei, weil es mit dem Maemo-Linux mitkam.

Share Button

Laptopnetzteile

Bei Mobiltelefonen hat es endlich geklappt, dass alle Telefone denselben Anschluss für USB-Kabel und Ladegerät unterstützen. Fast alle, nämlich genaugenommen alle außer Apple. Aber die überwältigende Mehrheit der Mobiltelefone hat denselben Anschluss und funktioniert mit denselben Ladegeräten.

So etwas wäre bei Laptops auch schön. Tatsache ist aber, dass jeder Hersteller eine Vielfalt von Modellen hat, die sich noch laufend ändern, typischerweise auch innerhalb derselben Modellbezeichnung, und die auch jeweils spezifische Netzteile brauchen. Wenn man sein Netzteil vergessen hat und in einen großen Computerladen geht, kann man das selbstverständlich bestellen, aber die Chance, dass ein passendes Teil in dem Laden oder in einem anderen Laden in einer größeren Stadt (z.B. Zürich) direkt erhältlich ist, ist vernachlässigbar. Das war schon vor 10-15 Jahren so.

Es wäre wirklich gut, wenn sich wie beim Mobiltelefon die Hersteller auf einen Standard für Netzteile für alle Laptops (meinetwegen wieder alle bis auf einen Hersteller mit einem angebissenen-Obst-Logo) einigen könnten. Ich glaube, darüber würden sich viele Kunden freuen. Aber sie schaffen es ja heute nicht einmal innerhalb ihrer eigenen Marke, was angeblich der Hersteller mit halb gegessenen Apfel immerhin geschafft haben soll, zumindest mit einer größeren Adaptersammlung.

So werden wir also weiterhin viel Elektronikmüll produzieren, weil gute Netzteile in den Müll wandern, wenn der Laptop dazu kaputt ist, und wir werden weiterhin ein paar Stunden mit der Fahrt verbringen, wenn wir das Netzteil einmal vergessen haben.

Share Button

Scala Days in Berlin 2014

Deutsch

Around mid of June 2014 I have been visiting the Scala Days in Berlin. Like usual these events contain a lot of speeches, which were distributed in four tracks, apart from the key notes. The event location was a cinama, like the Devoxx in Antwerp, but this time one that has been transformed to something else many years ago, but good projectors were available. Major topics where issues about compiler construction which is a hard task, but looked at with the right functional perspective it can be derived from the simple task of writing an interpreter. This helps understanding language constructs in Scala, but the idea was applied to many other areas as well, for example for compiling and optimizing SQL queries and for analyzing source code.

Another major topic was „streams“, which can be useful for web services. Other than traditional web services which usually receive the whole request before starting to process it, concepts where discussed for dealing with large requests whose size is painful or impossible to keep at once in memory or who are even unlimited in size. These can also be applied to websockets. This demands processing data as soon as useful parts have arrived.

Another minor, but very interesting topic was development of Android apps with Scala. The commonly known approach is off course Scaloid, but an alternative, Macroid, was presented. It looked quite promising, because it allows to write nice Android apps with less code. A major worry is that scala apps consume too much memory due to their additional libraries. Because Scala uses its own libraries on top of the usual preinstalled Java libraries which are about 5 MB in size, this can easily anihilate the attractiveness of Scala for modern smart phone development, unless we assume rooted devices which have the Scala libs preinstalled. But that would seriously limit the scope of the app. This is not as bad as it sounds, because the build process contains one step in which unnecessary classes are removed, so that we only install what is really needed. When going as crazy as running Akka on the cell phone it becomes quite a challenge to configure this step because Akka uses a lot of reflection and so all these reflective entry points need to be configured, leaving a wide door open for bugs that occur at runtime when some class is not found.

API design was another interesting talk. Many ideas were quite similar to what I have heard in a API design traing for the Perl programming language by Damian Conway a couple of years ago, but off course there are many interesting Scala specific aspects to the topic. It is surprisingly hard to obtain binary compatibility of classes and this even forces to ugly compromises. So always recompiling everything looks tempting, but is not always reasonable. So ugly compromises remain part of our world, even when we are working with Scala.

Share Button

Scala Days in Berlin 2014

English

Am 16., 17. und 18. Juni 2014 war ich bei der Konferenz „Scala Days“ in Berlin. Wie so oft bei diesen Veranstaltungen gibt es einen Haufen Vorträge, in diesem Fall bis auf die jeweilige „Keynote“ jeweils vier gleichzeitig. Das Veranstaltungslokal war wie bei der Devoxx in Antwerpen ein Kino, allerdings in diesem Fall schon lange umgewidmet für andere Zwecke, aber gute Projektoren gab es noch. Große Themen waren Fragen des Compilerbaus und wie man mit der richtigen funktionalen Perspektive aus der relativ einfachen Aufgabe, einen Interpreter zu schreiben, zu der schwierigen Aufgabe kommt, einen Compiler zu schreiben. Das hilft sicher, Sprachkonstrukte in Scala zu verstehen, aber die Idee wurde auch auf andere Felder angewendet, etwa um SQL-Queries zu kompilieren und zu optimieren oder um mit einem aus Compilercode gebauten Programm Quelltexte zu analysieren.

Ein anderes großes Thema waren „Streams“, die für Webservices nützlich sein sollen. Im Gegensatz zu klassischen Webservices, bei denen man den ganzen Request erstmal in Empfang nimmt, wurden auch Konzepte behandelt, um sehr große oder quasi unbegrenzte Requests zu verarbeiten. Dazu muss man diese natürlich schon verarbeiten, sobald eine gewisse nutzbare Datenmenge angekommen ist.

Ein kleines, aber interessantes Thema war die Entwicklung von Android-Apps mit Scala. Bekannt ist als Ansatz dafür natürlich Scaloid, aber hier wurde Macroid, ein alternativer Ansatz vorgestellt. Es sah vielversprechend aus, dass man mit weniger Code gute Android-Apps schreiben kann. Eine große Sorge ist, dass diese Scala-Apps den Speicher sprengen. Weil sie zusätzlich zu den vorinstallierten Java-Libraries noch Scala-Libraries brauchen, die etwa 5 MB groß sind, vergeht einem schnell der Appetit, außer man setzt gerootete Android-Devices voraus, auf denen die Scala-Libraries vorinstalliert sind. Das Thema verliert aber ein bißchen seinen Schrecken, weil der Build-Prozess einen Schritt enthält, in dem unnötige Klassen aussortiert werden, so dass am Ende nur das installiert wird, was man wirklich braucht. Wenn man so weit geht, Akka auf dem Mobiltelefon laufen zu lassen, wird das aber spannend, weil Akka viel Reflection und damit sehr viel fehleranfällige Konfiguration für diesen Optimierungsschritt benötigt.

Interessant war auch das Thema API-Design. Vieles war deckungsgleich mit Dingen, die ich bei einer API-Design-Schulung für Perl von Damian Conway vor ein paar Jahren gehört hatte, aber es gibt natürlich Scala-spezifische Themen, die auch interessant sind. Es ist erstaunlisch schwierig, Binärkompatibilität von Klassen zu erzielen und zwingt auch zu unschönen Kompromissen. Aber die gehören wohl auch in der Scala-Welt zum Leben.

Share Button

Closures in C and Scala

Deutsch

Are closures at all possible in C, without falling back to writing some interpreter in C and using that interpreted langauge?

Function pointers alone are far less than what is needed for closures. But they are one of the building blocks. It is quite hard to get the signature right, but a typedef proves to be useful.

The next issue is that C does not allow inner functions by default and that it is not possible to automatically include a context, which is essential for the concept of closures.

But it is surprisingly easy to overcome that issue:

The function is defined before the function within which it should be meant to be defined. It has an additional parameter for some „context“-struct, which can be used to include variables from that context.

struct closure;
typedef int (*fun_type)(const struct closure *context, const int param);

This struct includes the variables and a function pointer:

struct closure {
int x;
fun_type fun;
};

Now the function definition in languages that support closures still have to be provided in some way and this is done anonymously with some mechanism called lambda or so, within another function or method whose variables are implicitely included. In a way methods can be considered a special case of this, since they include access to attributes of the enclosing object. In C all functions are defined in the regular way, but this time the fun_type signature needs to be observed. References to enclosed variables need to be bound by explicitely putting them into the context:

int f(const struct closure *context, const int param) {
return (context->x) + param;
}

The second order function that returns the closure can now be defined. We only have to accept the C notation, but it is fully equivalent to closures, just a little bit more noise:

struct closure *adder(int x) {
struct closure *result = malloc(sizeof(struct closure));
result->x = x;
result->fun = f;
return result;
}

Off course memory management is always an issue to observe in C…

Now the whole thing can be used like this:

int main(int argc, char *argv[]) {
int retcode;
if (argc < 2) { usage(argv[0], "not enough parameters"); } int x = atoi(argv[1]); int y = atoi(argv[2]); struct closure *cl = adder(x); int i; for (i = 0; i < y; i++) { printf("cl(%d)=%d\n", i, cl->fun(cl, i));
}
}

The complete example can be found on github.

Off course the same is much shorter and more elegant in Scala:

object Closure {
def main(args : Array[String]) : Unit = {
val x : Int = args(0).toInt
val y : Int = args(1).toInt
val f : ((Int) => Int) = adder(x);
val arr = (1 to y).map(f)
println(arr.toString)
}

def adder(x : Int) : ((Int) => Int) = {
(y => x+y)
}
}

Even this can be found on
github.

Is there a way to achieve Closures in C describes numerous approaches to this issue.

Share Button