Direkt zum Hauptbereich

Kontrolltechnik: Verzögerung

Bei imperativen (= befehlsorientierten) Programmiersprachen wie Python, Ruby, Java oder C# herrscht das "Gesetz des nächsten Befehls" -- so nenne ich das jedenfalls gerne. Nach der Ausführung eines Befehls wird unweigerlich der nächste Befehl abgearbeitet, dann der übernächste, dann der überübernächste usw. Die Anweisungen werden schlicht in Reihe (sequentiell) abgearbeitet. Eine Ausnahme davon bilden Sprünge. Eine Sprunganweisung erlaubt es, mehrere Anweisungen vor oder zurück zu springen. Ist die Sprunganweisung an eine Bedingung geknüpft, dann kann bedarfsweise gesprungen werden. Ohne bedingte Sprünge wäre das Programmierleben ziemlich trist. Es gäbe sonst nur Programme, die Befehle "von oben nach unten" abarbeiteten und schlimmstenfalls in einer Endlosschleife gefangen blieben.

An dem "Gesetz des nächsten Befehls" ist nicht zu rütteln. Vordergründig ist ein Programm in einer imperativen Sprache als Kette von Anweisungen zu schreiben und zu verstehen. Der Kontrollfluss, der thread of control, folgt den Anweisungen. Sobald ein Programmierer aber irgendetwas Interessantes machen möchte, ist er zwar gezwungen dieses vordergründige Ausführungsschema beizubehalten, tatsächlich aber wird er in die "Trickkiste" greifen, um sich aus dieser Abhängigkeit zu lösen. Zwei dieser wesentlichen "Tricks" möchte ich Ihnen heute vorstellen: verzögerter Aufruf und verzögerte Ausführung. Statt "verzögerter Aufruf" könnte man auch "verzögerte Kommunikation" sagen, da in imperativen Sprachen Aufrufe das Mittel zur Kommunikation zwischen Programmteilen sind. Als Programmierer werden Ihnen beide Techniken bekannt sein, nur vielleicht nicht unter diesem Namen. Und vielleicht haben Sie beide Techniken auch noch nicht auf diese Weise in "Reinkultur" betrachtet.

Verzögerter Aufruf

Nehmen wir folgendes Programmfragment in der Sprache Python. Zunächst binden wir die Bibliothek "math" ein, dann definieren wir die Funktion "calc". Die Funktion nimmt eine Zahl entgegen, ruft die "Wurzelzieh"-Funktion aus der Mathe-Bibliothek aus, addiert Eins dazu und liefert das Ergebnis zurück.

import math

def calc(n):
x = math.sqrt(n)
return x + 1

Wenn eine andere Funktion aufgerufen wird, hier "math.sqrt", dann folgt der Kontrollfluss dem Aufruf. Es handelt sich also um einen unbedingten (nicht von einer Bedingung abhängigen) Sprung! Bevor der Sprung zu der anderen Funktion ausgeführt wird, merkt sich die Python-Ausführungsmaschine zum einen den aktuellen Wert von Variablen in "calc", hier den Wert von "n". Zum anderen wird sich gemerkt, an welcher Stelle die Ausführung nach dem Aufruf von "math.sqrt" fortzusetzen ist, d.h. an welche Stelle nach der Ausführung von "math.sqrt" zurück zu springen ist. Diese Information wird auf einem sogenannten Call-Stack abgelegt.

Was können wir tun, wenn wir den Aufruf (call) "math.sqrt(n)" nicht sofort ausführen, sondern vorläufig zurückstellen wollen?

Statt des Aufrufs kapseln wir alle zum Aufruf benötigten Informationen in einer Datenklasse "DelayedCall" (verzögerter Aufruf):

class DelayedCall(object):
def __init__(self,func,*args,**kw):
self.func, self.args, self.kw = func, args, kw

Auch wenn Sie den Konstruktor ("__init__") von DelayedCall in Python nicht gleich verstehen, ist es ganz einfach, was hier getan wird. Eine beliebige Anzahl übergebener Argumente wird als Liste gespeichert in "self.args". Benamte Argumente werden als Dictionary (HashMap) in "self.kw" abgelegt.

Der zurückgestellte Aufruf verändert unser Programmfragment wie folgt:

def calc(n):
x = DelayedCall(math.sqrt,n)
return x + 1

Nun hält "x" nicht mehr das Ergebnis eines Wurzelaufrufs, sondern einen zurückgestellten Aufruf! Der Aufruf kann jetzt jederzeit nachgeholt werden via

x.func(*x.args,**x.kw)

Allerdings haben wir jetzt ein neues Problem. Wir können nicht einfach mit "return x + 1" fortfahren. Die Addition arbeitet mit Zahlen und nicht mit einem DelayedCall-Objekt.

Verzögerte Ausführung

Stellen wir doch die Ausführung von Anweisungen nach einem verzögerten Aufruf ebenfalls zurück! Auch das ist ganz einfach: Wir kapseln jetzt die ausstehenden Anweisungen in einer Funktion, die wir "callback" nennen. Als Input verarbeite die Funktion das Ergebnis des zurückgestellten "math.sqrt"-Aufrufs.

def calc(n):
x = DelayedCall(math.sqrt,n)
def callback(x):
return x + 1
return x, callback

Der Ursprungscode für "calc" lässt sich noch erkennen, aber es hat sich einiges verändert. Die Funktion "calc" führt weder den Aufruf, noch die darauf folgenden Anweisungen aus und gibt auch keine Zahl mehr als Ergebnis zurück. Stattdessen gibt "calc" das Objekt mit dem gekapselten Aufrufparametern und eine Funktion namens "callback" zurück. Nun ist es an jemand anderem, (z.B dem Aufrufer von "calc") Aufruf und Ausführung nachzuholen.

callback(x.func(*x.args,**x.kw))

Die Bedeutung von "calc" hat sich vollkommen verändert. Es ist keine Funktion mehr, die eine Wurzel berechnet und das um Eins erhöhte Ergebnis zurück liefert. "calc" ist zum Konstruktor geworden für "Aktionskapseln", die zwar ebendieses tun, aber eben zu einem späteren Zeitpunkt.

Dieses Ablösung dessen, was das "Gesetz des nächsten Befehls" verlangt und was ein Programmierer durch solche Techniken intendiert (beabsichtigt), ist der Grund dafür, warum Programme im imperativen Stil oft so schwer zu durchschauen sind. Man muss erst herausfinden, was der Programmierer da eigentlich treibt, bevor man zur eigentlichen Programmlogik vordringt.

Und noch eines lässt sich an diesem Beispiel sehr schön zeigen: Diese Techniken greifen auf der Code-Ebene und zielen darauf ab, den Kontrollfluss zu manipulieren. Die UML (Unified Modeling Language) ist eigentlich nicht dafür geeignet, solche Details abzubilden. Dabei sind diese Details enorm wichtig, da sie die Code-Ausführung erheblich beeinflussen und damit die Semantik eines Programms verändern.

Beliebte Posts aus diesem Blog

Lidl und der Kassen-Bug

Es gibt Fehler, im Informatiker-Jargon "Bugs", die etwas anrühriges haben. Ich bat den Menschen an der Kasse bei Lidl um einen Moment Geduld und meine Kinder um Ruhe, um nicht den wunderbaren Moment zu verpassen, bei dem es passierte. Der Lidl-Mensch fluchte kurz auf -- und ich war entzückt! "Einen Moment, davon muss ich ein Foto machen!" Und dann machte ich noch eines. Ich bin heute extra für diesen Fehler zu Lidl gepilgert -- ich wollte es mit eigenen Augen sehen. Gestern hat mir ein Student (vielen Dank Herr Breyer) von diesem Fehler in einer EMail berichtet. Ein richtig schöner Fehler, ein Klassiker geradezu. Ein Fehler, den man selten zu Gesicht bekommt, so einer mit Museumswert. Dafür wäre ich sogar noch weiter gereist als bis zum nächsten Lidl. Der Fehler tritt auf, wenn Sie an der Kasse Waren im Wert von 0 Euro (Null Euro) bezahlen. Dann streikt das System. Die kurze Einkaufsliste dazu: Geben Sie zwei Pfandflaschen zurück und Lidl steht mit 50 Cent bei Ihne

Syntax und Semantik

Was ist Syntax, was ist Semantik? Diese zwei Begriffe beschäftigen mich immer wieder, siehe zum Beispiel auch " Uniform Syntax " (23. Feb. 2007). Beide Begriffe spielen eine entscheidende Rolle bei jeder Art von maschinell-verarbeitbarer Sprache. Vom Dritten im Bunde, der Pragmatik, will ich an dieser Stelle ganz absehen. Die Syntax bezieht sich auf die Form und die Struktur von Zeichen in einer Sprache, ohne auf die Bedeutung der verwendeten Zeichen in den Formen und Strukturen einzugehen. Syntaktisch korrekte Ausdrücke werden auch als "wohlgeformt" ( well-formed ) bezeichnet. Die Semantik befasst sich mit der Bedeutung syntaktisch korrekter Zeichenfolgen einer Sprache. Im Zusammenhang mit Programmiersprachen bedeutet Semantik die Beschreibung des Verhaltens, das mit einer Interpretation (Auslegung) eines syntaktisch korrekten Ausdrucks verbunden ist. [Die obigen Begriffserläuterungen sind angelehnt an das Buch von Kenneth Slonneger und Barry L. Kurtz: Formal Syn

Factor @ Heilbronn University

It was an experiment -- and it went much better than I had imagined: I used Factor (a concatenative programming language) as the subject of study in a project week at Heilbronn University in a course called "Software Engineering of Complex Systems" (SECS). Maybe we are the first university in the world, where concatenative languages in general and Factor in specific are used and studied. Factor is the most mature concatenative programming language around. Its creator, Slava Pestov, and some few developers have done an excellent job. Why concatenative programming? Why Factor? Over the years I experimented with a lot of different languages and approaches. I ran experiments using Python, Scheme and also Prolog in my course. It turned out that I found myself mainly teaching how to program in Python, Scheme or Prolog (which still is something valuable for the students) instead of covering my main issue of concern: mastering complexity. In another approach I used XML as a tool