Montag, Februar 25, 2008

Wie lerne ich eine Programmiersprache an einem Tag?


(Foto von jbvkoos)

Lassen Sie sich mal an einem Tag für acht oder zehn Stunden, oder wenn es Sie so richtig gepackt hat, für 12 Stunden auf eine neue Programmiersprache ein! Sie werden sehen, dass Sie erstaunlich weit kommen. Weiter als Sie es vielleicht für möglich gehalten haben. Das geht, wenn Sie sich der Sache ein wenig mit System nähern. Hier ein paar Tipps und Vorschläge. Ich beziehe mich im Folgenden beispielhaft auf JavaScript.

Bevor es los geht, ein kleiner Hinweis: Sie werden keine Sprache an einem Tag lernen können, wenn Sie nicht bereits mit mindestens einer modernen Programmiersprache gut vertraut sind. Die erste Hürde muss also schon genommen sein. Ab der ersten Hürde wird es immer leichter ;-)

Vorbereitung: Besorgen Sie sich ein Buch zu der Programmiersprache, die Sie interessiert. Am besten in Papierform -- mit Papier lässt sich sehr schnell und zügig arbeiten. Legen Sie sich für Ihre Streifzüge durch das Buch Stift und Textmarker bereit. Beschaffen Sie sich den Standard zu Ihrer neuen Programmiersprache. Meist finden sie ihn frei zugänglich im Web. Ein Standard ist ein technisches Dokument, das die Programmiersprache genau beschreibt und definiert. Manchmal gibt es keinen formalen Standard, sondern ein Buch oder Dokument, das die Sprache verbindlich definiert. Suchen Sie danach. Und zu guter letzt: Installieren Sie sich die neue Sprache auf Ihrem Rechner, damit Sie sofort damit herumspielen können. Ja, spielen -- die Sache soll Spaß machen!

Um all das kümmern Sie sich am besten ein paar Tage bevor Sie sich mit der Sprache dann für einen Tag beschäftigen wollen. Los geht's!

Syntax anschauen: Beginnen Sie mit einen Blick in die Syntax, also den Regeln, wie gültige Programme aussehen dürfen. Die Syntaxregeln werden Sie vermutlich im Standard finden, meist im Anhang. Viele Bücher zu Programmiersprachen verzichten bedauerlicherweise sogar auf eine klare Angabe der Syntaxregeln; jetzt wissen Sie, warum Sie ihn sich beschaffen sollten. Schauen Sie zum Beispiel in den Annex A des Standards zu JavaScript, ECMA-262, 3rd Edition. Auf wenigen Seiten erfahren Sie interessante Dinge über die Sprache, etwa eine Liste reservierter Schlüsselwörter (keywords). Von "break", "case", "catch", ... bis hin zu "var", "void", "while", "with". Ganze 25 Schlüsselwörter gibt es zu erkunden. Damit gibt es ein erstes klares Arbeitsprogramm: Verstehen, wofür jedes dieser Schlüsselwörter da ist. Verbergen sich hier neue, interessante Konzepte?

In der Syntaxbeschreibung werden Sie bei JavaScript zum Beispiel auch entdecken, dass reguläre Ausdrücke zur Sprache gehören. Wenn Ihnen das nichts sagt: unbedingt lernen! Nicht jetzt, an diesem Tag -- sonst kommen wir nicht durch -- aber reguläre Ausdrücke sind ein mächtiges Werkzeug bei der Suche nach Mustern in Zeichenketten. Muss man können! Hat praktisch jede moderne Sprache mindestens als Bibliothek im Gepäck.

Welche Datentypen gibt es? Jetzt kann Ihr Buch zum Einsatz kommen. Blättern Sie durch das Inhaltsverzeichnis, verschaffen Sie sich einen Überblick über die Themen. Beschränken Sie sich rein auf Kapitel zu Ihrer neuen Sprache. Ignorieren Sie Bibliotheken, Erweiterungen oder Spezialanwendungen. Das ist jetzt alles uninteressant. Fangen Sie an mit den Datentypen: Welche Basistypen gibt es, welche "Sammeltypen" gibt es? JavaScript hat lediglich drei primitive Datentypen: Zahlen, Zeichenketten und Booleans. Dazu kommen noch zwei triviale Typen: null und undefined. Daneben gibt es noch den Datentyp des Objekts als Sammeltyp. Und das war es schon -- JavaScript präsentiert sich erstaunlich einfach. Hätten Sie's gedacht? Zwar redet man auch von Arrays, die jedoch ebenfalls Objekte sind; Funktionen, Zeitwerte, reguläre Ausdrücke und Fehler sind lediglich besondere Arten von Objekten.

Ein wichtiger Punkt ist noch: Wie wird mit Typen umgegangen? Handelt es sich bei Ihrer neuen Programmiersprache um dynamische oder statische Typen? Sie wissen vielleicht, das dynamisch typisierte Sprache, wie Python, Ruby oder JavaScript, zunehmend an Boden gewinnen. Die Art mit solchen Sprachen zu arbeiten ist eine gänzlich andere als mit statisch typisierten Sprachen wie Java oder C#. Noch wichtiger jedoch ist: Wie werden Datenwerte manipuliert? Wie heißt es so schön im Informatik-Slang: by value oder by reference? Wie werden Daten zugewiesen, wie wird beim Aufruf von Methoden, Prozeduren, Funktionen mit ihnen umgegangen? In JavaScript ist die Welt einfach geregelt: Primitive Typen werden "by value" behandelt, Objekte "by reference". Nur Strings passen nicht so ganz in das Schema. Strings sind immutable, nicht veränderbar, weshalb es keine Rolle spielt, wie sie behandelt werden.

Welche Anweisungen gibt es? Viele Anweisungen werden Ihnen bekannt vorkommen, wie z.B. "if", "switch", "while", "break", "return" etc. Suchen Sie nach Abweichungen, nach Anweisungen und/oder deren Verwendung, die Sie so aus Ihnen bekannten Sprachen nicht kennen. Den meisten ist die "for"-Schleife aus JavaScript bekannt, nicht jedem jedoch das "for/in".

Wie wird mit Variablen umgegangen? Müssen Variablen deklariert werden? Muss der Typ einer Variablen mit angegeben werden? Wie ist der Scope einer Variablen? Wenn Sie noch nie etwas vom lexikalischen Scope gehört haben -- jetzt müssen Sie sich damit auseinander setzen, denn die meisten Programmiersprachen arbeiten mit lexical scope, so auch JavaScript. Allerdings kennt JavaScript keinen Block-Scope, wie er sich zum Beispiel durch ein Schleifenkonstrukt wie "for" ergibt.

Fundamental ist das Konzept der Variablen, wenn Sie sich als Kenner einer imperativen (anweisungsorientierten) Sprache plötzlich mit einer funktionalen Sprache, wie z.B. Haskell oder Scheme/Lisp, beschäftigen. Plötzlich halten Variablen ihre Werte nicht über die Lebensdauer eines Funktionsaufrufs hinaus. Dauerhafte Wertzustände werden nicht mehr durch Objekte gekapselt; überhaupt arbeitet man in der funktionalen Welt niemals mit impliziten Zuständen. Scheme und Lisp erlauben zwar Ausnahmen, aber Sprachen wie Haskell sind da sehr konsequent und bringen einen mit vollkommen neuen Dingen in Berührung wie zum Beispiel der Zustandsmonade.

Welche Operatoren gibt es? Plus "+", Minus "-", Objekt-Schöpfung "new" etc., all das sind Operatoren. Ohne Operatoren läuft nicht viel. Hier verstecken sich die eigentlichen Arbeitsknechte. Meist sind für Operatoren Vorrangsregeln definiert (wie "Punkt- vor Strich-Rechnung"), die man grob kennen sollte. Ganz wichtig ist es zu klären, was in Ihrer neuen Sprache Gleichheit (equality) von Identität (identity) unterscheidet -- hier spielen "by value" und "by reference" wieder eine wichtige Rolle. Ein anderer wichtiger Operator ist die Zuweisung.

Welche speziellen Konzepte gibt es, die wesentlich für die Sprache sind? Für diese Frage ist es sehr schwer, eine Richtschnurr vorzugeben. Sie müssen zum Kern der Sprache vordringen. Was ist hier anders? Welche Konzepte, welche Konstrukte gibt es, die wesentlich den Charakter und die Denke der Programmiersprache prägen? In JavaScript sind es Funktionen (JavaScript hat ein sehr funktionales Herz) und Objekte/Arrays. JavaScript ist nicht, wie Sie es vermutlich von den meisten OO-Sprachen her kennen, klassen-basiert, sondern prototyp-basiert -- und kennt auch eine Form der Vererbung. Wenn das neu für Sie ist, dann ist hier ein Einstieg gefunden in die Welt von JavaScript. In den Objekten und Funktionen liegt der andere Ansatz verborgen, der JavaScript auszeichnet. Sie beherrschen JavaScript nicht, wenn Sie an dieser Stelle auf einen Abstieg in die Tiefen von JavaScript verzichten. Wenn Sie sich danach noch mit Modulen und Namensräumen beschäftigen, dann haben Sie JavaScript erobert.

Spielen Sie herum -- aber bleiben Sie fokussiert! Tippen Sie kleine Programmschnipsel in einen Editor oder eine IDE, machen Sie Ihre ersten Gehversuche mit der neuen Sprache. Dann prägen sich mache Dinge schneller ein oder man kann sein Verständnis für das eine oder andere Feature austesten. Aber lassen Sie sich nicht ziellos treiben. Ihr Ziel ist, an diesem einen Tag eine neue Sprache so gut und systematisch zu erfassen, wie möglich. Wenn Ihnen etwas Interessantes über den Weg läuft, notieren Sie es sich für später.

Wie geht es nach so einem Tag weiter? Das hängt ganz davon ab, warum Sie sich mit der neuen Sprache auseinander gesetzt haben. Wenn Sie sich weiter mit der Sprache beschäftigen wollen, nutzen Sie die Sprache für Ihr nächstes kleines Projekt oder Vorhaben. Learning by doing ist noch immer der erfolgreichste Weg.

Natürlich bedarf es einiger Wochen bis Monate, bis Sie sich in eine neue Sprache wirklich eingefunden haben; bis sie anfangen, in der Sprache zu denken und fließend in ihr zu sprechen. Mir sind schon des öfteren Talente über den Weg gelaufen, die in acht bis zwölf Wochen von Neulingen zu echten Experten in einer Sprache heranwuchsen. Das gelingt aber nur zusammen mit einem Projekt, das einem täglich die Auseinandersetzung mit der neuen Sprache abfordert.

---

Verraten Sie es mir: Hat Ihnen dieser Beitrag und seine Thematik gefallen? Haben Sie noch weitere Tipps für das Erlernen einer Programmiersprache?

Mittwoch, Februar 13, 2008

Vom Entwurf zum Programm




Was lernt man nicht alles für feine Dinge als Softwaretechniker: Die Software-Entwürfe entstehen am Reißbrett, oftmals hart erarbeitet, und sie präsentieren sich als schicke Diagramme. Dabei bedienen wir uns einer "Geheimnotation" namens Unified Modeling Language (UML), die für Laien kaum verständlich ist. Da gibt es Kästchen, Linien, Rauten, Zahlen, Benamungen, Textanmerkungen -- das wirkt alles sehr imposant. Es braucht einiges an Zeit, ehe man die hohe Kunst des Software-Entwurfs erlernt hat.

Irgendwann findet man sich jedoch auf der Programmier-Baustelle wieder. Hier werden die Entwürfe in für einen Computer verständliche Anweisungen umgesetzt. Und, wenn man genau hinschaut, geht es hier schlimmer zu als auf jeder echten Baustelle. Der Programmcode hat nur lose Ähnlichkeiten mit den Entwürfen. Programmierer ziehen mal hier und mal dort eine Wand ein, halten an anderer Stelle einen Durchbruch für angemessen, und das Maßband scheint ihnen völlig abhanden gekommen zu sein. "Pi mal Daumen" -- ja, so ungefähr entspricht das dem Entwurf. Wer seine Aufgabe als Bauherr auf der Programmier-Baustelle ernst nimmt, der kann verzweifeln.

Ein kleines Beispiel gefällig?

Schauen Sie sich mal oben das Bild an. Ein UML-Diagramm, das ich in einer Vorlesung nutze. Es ist ein Entwurf für ein OO-Framework. Hübsch anzusehen, gell? Ich weiß, das Diagramm ist nicht perfekt -- aber darauf kommt es mir hier nicht an.

Zu diesem OO-Framework gibt es eine einfache Implementierung in Python von mir. Auch die spreche ich mit den Studierenden durch. Vor wenigen Wochen, als ich versuchte, den Code zu erklären, griff ich zur Kreide. Ich begann, die Referenzbezüge, so wie sie im Python-Code aufgebaut sind, in einer informellen Notation an die Tafel zu malen, in der Hoffnung, so werde sich der Wald lichten. Tat er aber nicht. Das Ganze entwickelte sich zu einer wunderschönen Fallstudie, wie schwer sich Entwurf und Implementierung tun, aufeinander zu passen.

Was haben die beiden Bilder gemeinsam? Man erkennt Ähnlichkeiten. Die großen Kästchen tragen die gleichen Namen. Aber sonst? An der Tafel sind verwirrend viele Linien hinzugekommen. Wie kommt's?

Ein Kernproblem ist, dass die Ausdrucksmittel aus dem UML-Diagramm keine klare Entsprechung in meiner Zielsprache, der Programmiersprache Python haben. Ich muss also mit "Workarounds" arbeiten, die die Intentionen aus dem Entwurf umsetzen. Zum Beispiel die Kompositionen. In der UML baut eine Komposition besondere Zugriffsbezüge und Abhängigkeiten in den Lebensbezügen auf. In den gängigen OO-Sprachen, wie auch in Python, Java, C# oder Ruby gibt es jedoch keine Komposition. Die meisten OO-Sprachen basieren auf Objekt-Referenzen, die keine Zugriffsbezüge durch Namensräume regeln, jedoch auf den Lebenszyklus von Objekten einen erheblichen Einfluss haben. Also bildet man die Intention der Komposition auf anderen Wegen ab und sorgt sich um einen gesitteten Umgang mit Referenzen. Ähnliches gilt für das "instanceOf".

Und hier gehen die Probleme los. Wer das intendierte Mapping von Code und Entwurf nicht kennt -- dieses Mapping wird nämlich selten, eher gar nicht dokumentiert --, der steht in der Gefahr, irgendwann einmal am Code Änderungen vorzunehmen, die den Entwurf verletzen und der austarierten Balance von Code und Entwurf radikal Schaden zufügt. So entstehen Fehler, die in aller Regel schwer zu entdecken, wenn, dann meist folgenreich sind und deren Beseitigung erhebliche Anstregungen kostet.

Ein anderes Beispiel. In Python nutze ich gerne Dictionaries (sie kennen ähnliches vielleicht als HashMaps oder assoziatives Arrays) -- und zwar als Caches für einen schnellen Zugriff auf Objekte unter einem Schlüssel. Es sind die "Qualifier" (die kleinen Boxen an den großen Boxen) auf dem Tafelbild. Streng genommen, mit Blick auf den Entwurf, bräuchte ich sowas nicht. Doch der Einsatz von Dictionaries macht den Python-Code oft einfacher und übersichtlicher. Der Preis: Ich entferne mich vom Entwurf. Die Dictionaries beginnen ein Eigenleben zu führen.

Wenn Sie Studentin oder Student der Informatik sind, dann haben sie wahrscheinlich zahllose, weitere solche Beispiele auf Lager, die einen erheblichen "Clash" aufzeigen zwischen Entwurf und Umsetzung. Bisweilen entsteht das UML-Diagramm zum Entwurf erst nachträglich, nach der Programmierung, und schönt und verhübscht die "Häßlichkeiten" des Codes erheblich.

Sind Codegeneratoren ein Ausweg, die aus UML-Diagrammen direkt den Programm-Code (z.B. für Python) erzeugen? Jaein. Zum einen ist es gar nicht so einfach, gute schematische Code-Templates für die Code-Generation zu entwickeln. Der Versuch, Komposition sauber in einer rein referenz-basierten Sprache nachzubilden, ist nicht wenig aufwendig und versucht einer Sprache etwas überzustülpen, wofür sie nicht gemacht wurde. Das ist zumindest bei einem Blick auf das Laufzeitverhalten nicht ganz außer acht zu lassen. Allein die Nachbildung von Singletons, ein lapidarer Stereotyp in der UML, ist nicht trivial, besonders wenn Singletons thread-safe sein sollen.

Doch das eigentliche Problem liegt in meinen Augen tiefer. Code-Generierung aus Entwürfen ist ok, sofern man einen ausführbaren Entwurf haben möchte. Nicht immer ist das sinnvoll. Wichtig ist zu erkennen, dass es einen Unterschied gibt zwischen Entwurf (Design) und Umsetzung (Implementierung). Eine Umsetzung muss einen Entwurf erfüllen. Nicht mehr und nicht weniger. Das wird wenig verstanden. Ich habe versucht in meinen Posts zu User, Domain und Realization Model darauf in einem anderen Kontext einzugehen. Denn häufig sehe ich nicht nur Studierende, sondern auch Profis einen Fehler machen: Es wird weniger über einen Entwurf nachgedacht! Es wird Programmcode mit Hilfe von UML-Diagrammen beschrieben, nicht ein Entwurf! Dann wird der Code-Generator nur zum Erfüllungsgehilfen von mit Diagrammen "gemaltem" Code.

Glauben Sie nicht? Wann haben Sie das letzte Mal Mehrfachvererbung angewendet? Das wird Ihnen als Ausdrucksmittel in der UML für Ihre Entwürfe angeboten und kann ganz hilfreich sein. Was? Mehrfachvererbung nutzen Sie nicht? Warum? Weil Sie Java- oder C#-Programmierer sind? Erwischt! :-)