Monday, November 29, 2010

Wie würde man einem Neuling Programmierung beibringen?

Wie kann man jemandem, der bisher noch nicht programmiert hat, schnell und einfach die moderne Programmierung erklären? Viele Neulinge verlieren sicherlich schnell die Lust an diesem Thema, vor allem dann wenn sie erst an der Uni mit dem Thema in Berührung kommen oder sich in der Schule mit Bit-Gattern und Low-Level-Techniken der strukturellen Programmierung oder gar Go-To's herumschlagen müssen.

Ich spreche hierbei von eigenen Erfahrungen: Die Programmierung habe ich mir selbst mit Q-Basic beigebracht, und es folgte ein langer und steiniger Weg über verschiedene komische Kurse in der Schule. Ich denke aber nicht dass diese Methode heutzutage noch wirklich sinnvoll ist - vor allem weil man dabei viel zu viel Zeit verbrennt.

Deshalb mein Vorschlag: Wir sollten versuchen, Neulinge in die moderne Programmierung einzuführen, und möglichst viele der alten rein technik- und hardwarebezogenen Fallstricke für den Anfang außen vor zu lassen. Heute muss niemand mehr Transistoren schweißen oder Assembler hacken! Dafür sollte gerade am Anfang viel mehr Zeit in die richtigen, nicht rein technikspezifischen Konzepte gesteckt werden: Imperative Programmierung lässt sich am besten mit grafischen Hilfsmitteln erlernen, bei denen man Algorithmen entwickeln muss um Zeichenanweisungen durchzuführen. Zusätzlich kann man aber auch von Anfang an die Mathematisch Geprägte Funktionale Programmierung vorstellen, z.B. anhand von einfachen Taschenrechner Applikationen, und zeigen wie die beiden Konzepte in der modernen Programmierung zusammenspielen.
Das wichtigste ist bei beiden Wegen, dem Neuling möglichst viel Rückmeldung zu geben und ihm die Möglichkeit zu schaffen, schnelle Erfolge in der Programmierung zu sammeln. Später muss dann das Bewusstsein für die Vermeidung von Redundanzen und das Erkennen und Abhandeln von Fehlern geschaffen werden.

Ein Entwickler, der in der Lage ist sein Wissen auch dem blutigsten Anfänger zu erklären, der ist meiner Meinung nach besser und kompetenter als selbst der genialste C-Kernelentwickler oder Haskell-Halbgott. Schließlich ist die Quintessenz unseres Jobs doch nicht nur, immer die neusten und tollsten Techniken auszuprobieren, sondern auch, Wissen zu verbreiten und Dinge einfach und richtig erklären zu können, oder? Wenn wir nur unsere eigenen Techniken verbessern, werden wir langfristig weniger effektiv sein als wenn wir in der Lage sind ein Team "auszuheben" in dem alle über ähnliche Fähigkeiten verfügen.

Viele meiner bisherigen Posts erscheinen in dem Zusammenhang vllt als das genaue Gegenteil - sie befassen sich mit komplizierten Themen des Programmiersprachendesigns - aber dienen sie nur der Vorbereitung meines Vorhabens: Ich analysiere Programmiersprachen nur deshalb so lange, weil ich nach den "richtigen" Konzepten suche um einfache und dennoch gute und leistungsfähige Sprachen zu identifizieren. Nur eine solche Sprache taugt dazu, einem Anfänger die Konzepte einfach und dennoch "richtig" zu erklären. Folglich muss die Sprache in der Lage sein, mit den Fähigkeiten des Entwicklers zu "wachsen" und nicht von vornherein ein Buch mit sieben (oder deutlich mehr!) Siegeln zu sein. Andererseits muss die Sprache aber auch "richtig" sein. Merke: Einfachheit kommt immer erst NACH der Richtigkeit bei einer für das Lernen geeigneten Sprache. (Das wars dann für euch, BASIC und PHP. Achja, UND Java..).

Für mich müsste eine solche Einführung auf sehr viel von dem, was bisher klassischerweise unterrichtet wird, verzichten und stattdessen ganz andere Fokusse setzen.
Ein paar Beispiele für Themen, die jeder Neuling - in noch festzulegender Reihenfolge - lernen und kennen sollte:
  • Das binäre Zahlensystem - nur die blutigen Grundlagen. Ohne das Verständnis für binäre Zahlensysteme macht es keinen Sinn, sich mit der Programmierung zu befassen.
  • Grundlagen der hardwarenahen Datentypen - Vom Bit zum Byte, zum Integer zu den Gleitkommazahlen. Wie Chars und Strings in diesen Mix mit hereinspielen (Zeichenkodierungen).
  • Imperative Programmierung mit grafischen Oberflächen a la Nikki, Logo
  • Algorithmen, mit denen häufig benutzter Code ohne Redundanzen auskommt
  • Einfache Schleifen und Selbstentwickelte Schleifen
  • Referenzen und Objekte
  • Objekte und Typen - Typen als Möglichkeit, Daten zu speichern UND Redundanzen zu vermeiden
  • Funktionen - Algorithmen ohne Seiteneffekte. Gerade er "Kein Seiteneffekt" Teil wird hier besonders im Mittelpunkt stehen.
  • Funktionen als Datentypen - Möglichst einfach und ohne den komplizierten Background
  • Unverändlerliche Objekte (Werte) und veränderliche Objekte
  • Mehrere Objekte - Collections.
  • Operatoren
Einige der typischsten Fragestellungen, die Dinge nur unnötig verkomplizieren, bleiben in dieser Betrachtung aussen vor: Call-By-Ref vs. By-Value, Arrays (nur als Teil der allgemeinen Collections), ...
Die o.g. Liste wirkt vllt heftig, aber eigentlich ist sie es gar nicht: Viele Konzepte lassen sich einem unbelasteten Neuling ohne technischen Hintergrund vllt. sogar leichter erklären als dem Programmiercrack, der in vielen Bereichen, z.B. den Closures, der imperativen Programmierung und der Objektorientierung bereits eine festgefahrene und schwerer zu ändernde Meinung hat, als jemand der das alles ganz unbedarft neu lernen kann. Das Ziel ist ja gerade, keine Sprachenblinde Sicht zu entwickeln sondern möglichst viele moderne Konzepte anhand der Sprache zu erklären und diese zu einem sinnvollen Gesamtbild zusammenzusetzen.

Neben der obigen (unvollständige) Liste wird aber vor allem noch eine passende Programmierumgebung benötigt. Nein.. es kann nicht Eclipse sein, wenn man die Leute nicht schreiend verjagen will. Es sollte eine deutlich abgespeckte Variante dieser IDE mit einer Besonderheit sein: Einer grafischen Oberfläche mit einem "Icon", dass sich durch Programmbefehle steuern und mit Anweisungen versehen lässt.

Kennt ihr Nikki, den Roboter und oder den Zeichenigel? Beide grafischen Lernumgebungen für Pascal bzw. Logo gehörten damals zu besten Umgebungen, die Programmierung direkt durch grafische Rückmeldungen zu lernen, und ich glaube bis heute hat sich an der Attraktivität einfacher grafischer Operationen für Anfänger nichts geändert. Die Umgebung sollte vllt nur mit einem ähnlich einfachen Befehlssatz "attraktivere" evtl. in 3-D gerendete grafische Elemente erzeugen. :-)

Mal sehen wie weit ich das Thema weiterhin verfolge!

Tuesday, November 2, 2010

@Deprecated: Call By Ref und Primitive ODER: Warum Call-By-Value und Immutables reichen

Im Mittelpunkt stehen hier einige der Konstrukte, die jeder angehende Informatiker stundenlang in der Schlule pauken musste: Call By Ref und Call By Value. Meiner Meinung nach zu Unrecht...

Bei all meinem Lästern über die Schwächen von Java muss ich doch mal an einer Stelle eine Lanze für die Sprache brechen, ebenso für C. Doch von vorn..

Eine der wegweisenden Entscheidungen bei der Entwicklung von C war meiner Meinung nach, in der Sprache nur Call-By-Value anzubieten. Nur Call-By-Value? Die meisten die C kennen, werden jetzt anmerken, dass doch gerade die Sprache für ihre wilden Pointerreferenzen bekannt geworden ist! Das ist richtig, aber es ist trotzdem kein eigenes Call-By-Reference-Sprachkonstrukt. Während andere Sprachen wie z.B. Pascal Call-By-Ref als syntaktisches Konstrukt in die Sprache einbauen mussten, war dies in C bewusst nicht vorgesehen.

Die Pointerarithmetik bot nur einen Ausweg, mit dem man Call-By-Ref über Umwege realisieren konnte - auch wenn dies meist sehr haklig war. Um genauer zu sein: Es wurde sogar als der Fluch von C gesehen. Es zeigte sich, dass eine starke Orientierung an der Maschine nicht immer eine gute Idee sein muss:

Implementierung in C

// this is C
void add(int* a, int b){
  *a = *a + b;
}

int a = 3, b = 4;
add(&a, b);
println(a);

Diese Methode soll zu einem Eingangswet a einen Wert b hinzufügen. Will man das Konstrukt als Call-By-Ref benutzen, d.h. einen Eingangsparameter ändern, dann muss man, wie man oben sieht, aus dem Eingangsparameter a einen Pointer machen. Außerdem muss die Methode add entweder mit einer Pointer-Variablen aufgerufen werden, oder wie hier, mit einem normalen Parameter und einem &: &a. (Dies liefert die Adresse von a zurück, wodurch a in der Methode als Pointer verwendet werden kann).

Da zur Zeit von C Call-By-Ref gang und gäbe war, ist dieses Vorgehen nicht unbedingt auf ungeteilte Begeisterung gestoßen. In C++ hat man es sogar als so dermaßen umständlich empfunden dass man die Referenzparameter erfunden hat:

Implementierung in C++

// this is C++
void add(int& a, int b){
  a = a + b;
}

int a = 3, b = 4;
add(a, b)
println(a);

Zugegebenermaßen schon etwas einfacher - C++ hat also Call-By-Ref gehabt. Allerdings ist das, wie vieles bei C++, mit einem Overhead an Komplexität erkauft worden: C++ hat jetzt mehrere Konstrukte zur Deklaration von Parametern, die man alle kennen muss: Werte, Pointer, Referenzen, ... Inbesondere beim Verwenden von Pointern, Referenzen und Werten auf Objekte konnte schnell Verwirrung entstehen.

Ich persönlich finde da C's Lösung, die auch alles kann, aber mit weniger Komplexität auskommt, schöner. Zumal C Call-By-Ref mit einer expliziten &-Syntax bestraft. Das einzige was hier dreckig ist, ist dass Pointer und Ints implizit ineinander umgewandelt werden. Ohne diese "Hilfestellung" wären wahrscheinlich Myriaden von Fehlern der Art "& bei Referenzparameter vergessen" niemals aufgetaucht. Überhaupt sind die impliziten Konvertierungen meist viel eher die Wurzel alles Bösen in C als die Pointerarithmetik. Aber egal....

Denn dann kam Java.

Java's Idee, woher auch immer sie geklaut war, war wirklich gut: Es gibt nur zwei Arten von Variablentypen: Werte und Referenzen. Keine Pointer mehr. Die Aufgabe der Pointer übernehmen nun konsequent die Referenzen. Gleichzeitig wurde die Regel erlassen, dass die Logik, ob etwas Wert oder Referenz ist, vom Typ abhängt: "Objekte" sind immer Referenzen, "Werte" hingegen werden direkt abgelegt.

Dadurch hat in Java nur noch eine Art, Variablen zu deklarieren - kein & oder * mehr! Call-By-Ref für einfache Werte ist nun überhaupt nicht mehr möglich. Das kann man als alter C-Hase auch als Nachteil empfinden...

1. Java-Versuch: Mit Werten

// this is java
void add(int a, int b){
  a = a + b;
}
int a = 3, b = 4;
add(a, b);
System.out.println(a);

Funktioniert nicht! a ist ein Wertparameter, der nicht von außen geändert werden kann! Hm.. Und nun? Die Lösung: Wir benutzen Referenzen, d.h. Objekte, weil über die ja Änderungen möglich sein sollten!

2. Java-Versuch: Mit Referenzen

// this is java
void add(Integer a, Integer b){
  a = a + b;
}

Integer a = 3, b = 4;
add(a, b);
System.out.println(a);

Möp. Funktioniert auch nicht! Die Zuweisung verändert nur die Referenz a, aber diese Änderung wird aber nicht nach Außen weitergegeben! Die einzige Möglichkeit, in Java Veränderungen von Eingangsparametern herbeizuführen, ist einen veränderlichen Wert hereinzugeben, und diesen zu verändern.
Und jetzt wirds interessant: Integer verhält sich genau wie int! Ich kann einen Integer nicht intern verändern. Objekte, die sich nicht intern verändern lassen, können in Java's Modell niemals als veränderliche Eingangsparameter benutzt werden!

Es gibt aber einen Ausweg:

3. Java-Versuch: "Array-Hack"

// this is java
void add(int[] a, int b){
  a[0] = a[0] + b; 
}

int[] a = {3};
int b = 4;
add(a, b);
System.out.println(a[0]);

Warum funktioniert das Ganze jetzt? Weil ein Array ein sogenanntes mutable Objekt ist, d.h. ein Objekt dass es erlaubt über seine externen Schnittstellen von außen intern geändert zu werden.
Integer ist hingegen ein immutable Objekt. Ein immutable Objekt kann nicht von außen geändert werden. Sein nach außen sichtbarer Inhalt (Obserable State) bleibt von seiner Erzeugung an über die gesamte Laufzeit konstant.

Immutable Objekte haben für Call-By-Value zwei Eigenschaften die sie sehr praktisch (für C-Hasen: sehr doof) machen:
  1. Sie können sicher an alle Methoden übergeben werden, ohne dass diese Methode eine Möglichkeit hat, eine Veränderung an dem Wert herbeizuführen. In Sprachen wie Java kommt dazu noch der Vorteil, dass es auch nicht mehr möglich ist, einer externen Variable einen entsprechenden Wert zu injecten (mangels Call-By-Ref).
  2. Sie verhalten sich in jeder Hinsicht genauso wie die Werte wie int, long, .. Besser formuliert: Normale Werte verhalten sich genauso wie immutable Referenz-Objekte.
So.. und jetzt kommt der wirklich interessante Teil:

Aus obiger (2) folgt: An allen Stellen, an denen man in Java Werte wie int, long einsetzt, kann man auch die immutable Referenz-Wrapper Integer, Long, .. einsetzen und semantisch identischen Code schreiben. Abgesehen von der schlechteren Performance und dem schlechteren syntaktischen Support durch die Sprache, versteht sich.

Daraus wiederum folgt: Eigentlich braucht man überhaupt keine einfachen Werte!
  1. Werte sind vollständig durch immutable Referenzen abbildbar, da sich diese genauso verhalten wie die Werte.
  2. Die in Java getroffene Entscheidung für mehr Performance ist Augenwischerei. Man kann einen Compiler ohne Probleme so schreiben, dass er die Entscheidung trifft, wann ein Wert eine Referenz sein muss und wann er als performanterer Wert abbildbar ist (in Java ist eine Verwendung in Collections nur mit Referenzen möglich, sonst können eigentlich fast immer direkt die Werte verwendet werden). Zusätzlich muss er noch die impliziten Konvertierungen von Werten in Referenzen durchführen et volia: Die letzte Stufe der obigen Vereinfachung ist erreicht.
Entering Scala:
  • Scala kennt die aus Java bekannten Primitiven nicht. Werte sind normale Objekte, die von einem besonderen Typ ("Value") ableiten. Durch die obigen Optimierungen erreicht der Compiler dieselbe Performance.
  • Semantisch kennt Scala nur noch das Konzept der Referenz auf der einen Seite und des Mutable bzw. Immutable Objekts auf der anderen Seite. Immutable Objekte treten dabei an die Stelle der obigen Primitiven wie int, long, boolean, .... 
  • Das verwendete Call-Konstrukt ist wie in Java immer Call-By-Value Mit Referenzen. Dies mag jetzt verwirren, aber Call-By-Value bedeutet, dass eine externe Variable immer unverändert bleiben muss. Call-By-Reference bedeutet dass eine Wertänderung sich auf die externe Variable auswirkt.
Scalas Typsystem hat eine Stufe der Optimierung erreicht, bei der mit sehr wenigen Konstrukten all das erreicht werden kann, was in anderen Sprachen aufwändiger und zum Teil nur mit expliziten Typkonvertierungen herbeizuführen ist. Auf Call-By-Ref wird aufgrund der unkontrollierbaren Seiteneffekte bewusst verzichtet. Meiner Meinung nach ist dies einer der Gründe, warum sich Scala so gut als Lernsprache eignet: Es delegiert mehr Entscheidungen an den Compiler als die anderen Sprachen und macht die wirklich richtigen Konstrukte einfacher.

Zum Abschluss
Eines sollte klar sein: Der einzig richtige Weg, das obige Beispiel zu konstruieren ist natürlich in allen obigen Sprachen wie folgt:

// this is scala
def add(a: Int, b: Int) = {
  a + b
}

var a = 3, b = 4
a = add(a, b)
println(a)

Das Beispiel sollte auch für List, Array oder SuperComplexDataTyp geltene:
Anstatt sich Gedanken darüber zu machen, mit welchen Semantiken man die Eingangsparameter überlädt, und wie man mutable Objekte sinnvoll gestalten kann, sollte man lieber eines tun: Mit Immutables arbeiten und neue Ausgangswerte erzeugen! => Funktional Programmieren.
Da Scala anders als alle anderen hier genannten Sprachen es auch erlaubt, einfach mehrere Ausgangswerte zu benutzen, stirbt der wichtigste Nutzen von Call-By-Ref einen stillen Tod: Es ist es nie nötig, mit dem  bösartigen und schwer zu lesenden Call-By-Ref zu arbeiten, nur um mehrere Ausgangswerte zu erzeugen.

Ich hoffe das diese "Beweisführung" zeigen konnte, dass man manchmal weniger Konstrukte mehr sind. Und das man bestimmte Entscheidungen, z.B. die von den Primitiven, am besten dem Compiler überlässt. In anderen Bereichen, wie der Garbage Collection, haben wir es ja auch schon getan. :-)

Monday, November 1, 2010

Imperativ vs Funktional - Krieg der Urgewalten II (gekürzte Fassung)

Hinweis: Den folgenden Artikel zu lesen macht nur dann Sinn, wenn man mit den Gedanken der funktionalen Programmierung vertraut ist:
Grundgedanke dieser Art der Programmierung ist, dass eine Funktion niemals ihre Eingangswerte verändern darf, sondern nur einen (neuen) Ausgangswert zurückliefern darf. Dieses akademische Konzept widerspricht dem weitverbreiteten imperativen Paradigma, das ein Programm komplett als eine zustandsbehaftete Abfolge von Befehlen sieht. (z.B. C, Pascal, Java). Beispiele für Sprachen mit einer funktionalen Orientierung sind Haskell, Clojure und teilweise Scala . 

Der Flamewar der beiden Lager der funktionalen und der imperativen Programmierung wird ähnlich erbittert geführt wie der zwischen den dynamischen und den statischen Sprachen. Oft genug scheint es die Praxis zu sein, dass funktionale und imperative Ansätze als konkurrierend betrachtet werden. Warum? Beide Ansätze haben ihre Vorzüge und Einsatzszenarien!

Imperativ
  • Entscheidend für echte Prozesse und Workflows.
  • Performanceoptimierungen sind am besten mit imperativen Konzepten möglich, da im Endeffekt auch jeder funktionaler Code spätestens durch den Compiler in eine imperative maschinenlesbare Form gebracht werden muss.
  • Leicht verständlich, so lange der Scope der Seiteneffekte beschränkt bleibt. Im privaten Scope von Objekten ist imperatives Denken z.B. sehr gut nachvollziehbar und meist auch leicht zu verstehen.
  • Stark imperative Ansätze, d.h. solche die die Eingangsparameter von Methoden verändern, statt neue Ergebnisse nur über die Ausgabeparameter zu liefern, führen schnell zu schwer verständlichen und schwer wartbarem Code. Im schlimmsten Fall muss man eine Kette von Funktionsaufrufen zum Ursprung zurückverfolgen, um herauszufinden wann wie wo ein Wert verändert wurde.
  • Imperative, veränderliche Objekte sind nicht teilbar. Sollen sie in unterschiedlichen Kontexten verwendet werden, wird man immer gezwungen sein, eine (defensive) Kopie des Werts anzulegen - oder schwer auffindbare Bugs heraufbeschwören.
  • Der Ansatz ist nicht threadsicher. Imperatives Denken funktioniert nur in deterministischen Scopes. Threads aber sind von ihrem Naturell her nichtdeterministisch.
  • Für die sichere Fehlerbehandlung bei Methoden ohne Rückgabewert können nur Exceptions verwendet werden - ein Konzept das nur in Java wirklich sicher ist, da es als einzige Sprache "checked" Exceptions kennt. Der Grund dafür ist dass man in allen anderen Fällen wie z.B. bei C nicht gezwungen ist, den Rückgabewert einer Funktion z.B. den Fehlercode zu prüfen. Nur Exceptions erzwingen eine Behandlung durch den Programmierer.

Funktional
  • Die Schnittstellen sind viel leichter verständlich: Eingangswerte können / werden nicht verändert, Ausgangswerte stellen (evtl. neue) Ergebnisse dar.
  • Funktionale Methoden sind test- und wartbarer: Sie beruhen niemals auf externen Zuständen und beeinflussen diese auch nicht. Dadurch sind sie auch generisch in unterschiedlichen Kontexten einsetzbar.
  • Funktionen sind gleichzeitig Werte, d.h. jede Funktion ist eine Closure die auch an andere Funktionen übergeben werden kann. Dadurch lassen sich Funktionen höherer Ordnung bauen, z.B. eigene for-each-Konstrukte.
  • Funktionale, immutable Objekte können beliebig geteilt werden, wodurch oft auf das Anlegen neuer Instanzen verzichtet werden kann. Das gleicht den Nachteil wieder aus, das man grundsätzlich eher mehr Objekte erzeugen muss als in imperativen Kontexten.
  • Ohne bestimmte Sprachstrukturen nicht durchführbar: Closures und Tupel müssen in der Sprache first class values sein. Zu Tupeln - mehrfachen Rückgabewerten - folgt bezeiten noch ein Artikel. Sonst einfach mal googlen.
  • Falls ein statisches Typsystem eingesetzt werden muss, sollte dies nach Möglichkeit strukturell sein um Typinferenz zu ermöglich. Soll die Sprache auch objektorientiert sein, dann kann das objektorientierte Denken in Klassen sehr leicht als Submenge des strukturellen Typsystems realisiert werden.

Man kann, unter Berücksichtigung einiger Bedingungen, beide Ansätze leicht vermischen. Da funktionale Programmierung immer nur eine Teilmenge sein kann, ist dies sicherlich ein vernünftiger Ansatz. Einige Dinge sind nur imperativ zu lösen, und jede Implementierung ist am Ende immer imperativ.

In der Praxis trifft man die beiden Ansätzen in Sprachen meist in der folgenden Form:
  • Hauptarten
    • Rein imperative Sprachen - in modernen Varianten meist mit "Objektorientierung". Oft sind keine Closures verfügbar, sie können es aber sein. (Java, C#, C++).
    • Rein funktionale Sprachen. Imperative Abläufe können nicht direkt in der Sprache ausgedrückt werden, sondern werden über ein besonderes Konstrukt ermöglicht, dass die Imperative Änderung funktional beschreibt: Die Monade. (Haskell)
  • Mischtypen
    • Eine funktionale Sprache mit minimalen imperativen Elementen. Veränderliche, imperative Konstrukte können trotz der funktionalen Orientierung mit geringem Mehraufwand eingebunden werden, werden aber nach Möglichkeit discouraged. (Clojure).
    • Eine imperative Sprache, die jedoch einen sehr starken Fokus auf den funktionalen Ansätzen hat und diese auch bevorzugt. (Scala, interessanterweise aber auch Javascript).