Tuesday, December 21, 2010

Meine wenigen Kritikpunkte an Scala

Liste ich mal kurz hier auf. Auch wenn ich ein großer Fan der Sprache bin - an einigen Stellen ist Scala meiner Meinung nach einfach unnötig komplex wenn es einfachere Wege gäbe.

  • OperatorPrecedence und Auswertungsrichtung: Beides sind meiner Meinung nach syntaktische Konstrukte die in Ausnahmefällen Vorteile bringen (mathematische Berechnungen) aber keinen inhärenten Mehrwert haben. Worin liegt der Vorteil, 3 + (4 * 2) in der Mathematischen Reihenfolge zu berechnen, wenn eine stupide Auswertung von links nach rechts á la Smalltalk viel einheitlicher gewesen wäre? Klammern als Gruppierungsmerkmal reichen doch aus.. Zur Richtung: Ich mag :: wirklich, aber ist es wirklich so wichtig?
  • MethodOverloading: Das hier ist ganz klar ein Erbe von Java, und wahrscheinlich unvermeidlich zu integrieren gewesen. In Scala ist es aber viel weniger nützlich als in Java, da strukturelle Typen und implizite Konvertierungen viele der Usecases abfangen. Dafür wird man dann mit den komplizierten Regeln und der Uneinheitlichkeit noch viel stärker abgestraft. Hätte man vllt. nur auf der Call-Side in einem Legacy-Mode, aber nicht in eigenen Klassen unterstützen sollen.
  • Unterschiedliche Notationsarten: Das ist wohl eines der schwierigsten Themen. Scala kennt die .-Notation, die Operatornotation und die Prefíxnotation zum Aufruf von Funktionen. Eine hätte gereicht - Meiner Ansicht nach sollte man nur die Operatornotation unterstützen, da diese die größte Flexibilität bietet und in dem Moment leicht lesbar wird, in dem das Method-Overloading und die Precedence ebenfalls weggefallen sind.
  • Einheitlichere Behandlung von Tupeln und Parameterlisten: Baut wiederum auf den vorherigen Punkten auf. Sind diese alle erfüllt, gäbe es weniger Corner-Cases bei denen der Compiler einen Syntaxfehler anmarkern würde, wenn Tupel und Parameterlisten nicht austauschbar verwendet werden können.
  • Wegfall der abgekürzten Schreibweise für Funktionen mit _ : Closures sind in kaum einer Sprache so leicht und Flexibel deklarierbar wie in scala: { () => println ("hello") }; {x => x + x}. Warum also hat man die noch stärker reduzierte Schreibweise mit dem Underscore eingeführt? Neben der meist abnehmenden Lesbarkeit ("ist das jetzt eine Funktion..?") gibt es auch unzählige Fallstricke bei der Deklaration. Wenn man Glück hat dann führen diese dazu dass einem der Compiler den Code vor die Füße wirft. Wenn man Glück hat...
  • Kein Inline-XML mehr: Das wäre wohl der erste Punkt auf der Liste der Schöpfer von Scala. Dieses Feature macht die Sprache nur komplizierter, ist an ein konkretes mittelmäßiges Dateiformat gebunden und macht Compilern und der IDE das Leben zur Hölle.
Wie man sieht kritisiere ich eigentlich nur die oft zu komplizierten Syntaxmöglichkeiten. Klar ist dass die Änderungen an der Sprache nicht vorgenommen werden, aber schade ist es trotzdem. Eine deutliche Vereinfachung der Syntax wäre zur Komplexitätsreduktion sicher viel hilfreicher als eine Reduktion von Features (implizite Konvertierungen...).
Ein weiterer Vorteil wäre dass vllt. eine Konzentration auf das wesentliche, d.h. Operationen zugunsten von weniger syntaktischen Quengeleien erreicht werden könnte. Zu oft scheint mancher Scala-Code nur vom Gedanken getrieben zu sein, eine möglichst mächtige DSL zu entwickeln.

Ausserdem habe ich auch noch einen Punkt den ich zusätzlich noch gerne "drin" sehen würde:
  • Zwanghafte Behandlung von Rückgabewerten außer (): Wenn Scala ähnlich wie Pascal das Entgegennehmen der Rückgabewerte zwanghaft machen würde, dann würde das Wegfallen der Checked-Exceptions weniger wehtun. In diesem Fall können Unit-Operationen mit Side-Effects dann als Rückgabewert Option[Exception] verwenden, und der Programmierer würde vom Compiler dazu gezwungen, diesen Wert zumindest entgegenzunehmen - und so nicht zu vergessen!

Monday, December 20, 2010

NeoLinq: Typdeklarationen

Die ersten Ideen zur Umsetzung des Typkonzepts...

Array = [ A ->
  length: Int
  isEmpty = length > 0
  subarray = {(pos: Int, count = length - pos) ->
    // native stuff
  }
]

String = [
  chars: Array[Char]
  isEmpty = chars.length > 0
  substring = { (pos: Int, count = chars.length - pos) ->   
    String new (
      chars = chars subarray (pos, count)
    )
  }
]

Diese Methoden stellen natürlich nur - nicht korrekte Beispielimplementierungen dar, bei denen noch viele interessante Fragen bzgl. des Designs zu klären sind:
1: Es wird später einen Weg geben müssen Methoden "nativ", d.h. in C zu implementieren. Dies ist der Weg, viele der abstrakten Konzepte und den Verzicht von Keywords sogar bei if und else dennoch performant zu gestalten.
2: Wird es default-parameter geben, d.h. defaults bei Tupeln? Der Code oben zeigt das alte Problem, dass diese auch direkt durchgereicht werden müssen. Vllt erübrigt sich dies aber, wenn ich wirklich das Tupel als ganzes durchreiche. Mal sehen.
3. Wie werden Konstruktoren funktionieren? Der Code oben zeigt den Weg, die "new" Funktion als generische Funktion des "Typ" Interfaces anzubieten, aber der weitere Code macht dann keinen Sinn.

Das oben ist noch kein ausformulierter Code, dient aber wie man sieht zur Definition von Klassen, d.h. implementierten Typen. Da das Typsystem vorsieht, auf oberster Ebene strukturell zu sein, und erst bei der Implementationsebene nominell, d.h. auf Klassenhierarchien, zu arbeiten, ergibt sich noch die Nötigkeit, Strukturelle Typen zu definieren.

Die Regel dazu lautet: Das ":"-Zeichen ist dem Typennamensraum vorbehalten. Dadurch ergibt sich für strukturelle Typen:

:Comparable = [
  compareTo(t :Any)
]
Dies ist ein struktureller Typ, auf den jeder Typ passt der eine CompareTo Methode hat.

Saturday, December 18, 2010

NeoLinq: Tupeldeklarationen

Eine der großen offenen Fragen ist, ob Tupelschnittstellen () zur Deklaration verwenden. Ich experimentiere damit, Typen stehts in [ ] einzuschließen, so möglich. Dann hätte jedes Konzept eine eigene Klammersprache:

(): Auswertungsreihenfolge
{}: Blockdefinition. Codeblöcke für spätere Ausführung.
[]: Typendefinition. Codeblöcke für spätere Ausführung UND public interfaces.


NUR durch Typen ist es möglich, zwischen Scopes zu wechseln! Das normale Scoping sieht vor, dass Namen nach unten durchlässig sind, d.h. in jedem Block sind die Werte der darüberliegenden Scopes bekannt, so sie nicht durch Name Shadowing, d.h. überlagen mit gleichen Namen, unsichtbar sind. Typen hingegen haben die Aufgabe, Schnittstellen nach oben sichtbar zu machen.

Zurück zu den Tupeln:
Es ist immer noch nicht definiert, was Tupel eigentlich sind. Was für eine Art von Typ stellen sie dar? Wenn man Tupel mit der []-Syntax für Typen deklariert, dann müssen Tupel vollwertig ins Typsystem passen bzw. DAS TYPSYSTEM ALS SOLCHES ERWEITERN. Verwendet man die ()-Syntax bei der Deklaration, dann werden Tupel nur als ein Typ unter vielen gesehen.
Anders formuliert:

[a: String, b: Int] oder (a: String, b: String)?


Wenn man das ganze so sieht scheint die zweite Variante besser zu sein. Tupel sollen Werte gruppieren und stellen eine Klasse von Typen dar. Aber die Mengen-Eigenschaft eines Tupels generisch auf einen Typen zu übertragen fühlt sich einfach falsch an.


Aber es gibt ja noch ein Konstrukt:
Ebenso wie das Typsystem ist auch das PatternMatching noch nicht ausformuliert, auch wenn es meiner Meinung nach ein ebenso fester Bestandteil sein muss.
PatternMatching kann große Auswirkungen auf Tupel haben: Genaugenommen ist (a: String, b:String) zum einen eine Deklaration, zum anderen aber ein Muster zum Zerlegen eines Typs in Variablen, inkl. (optionaler) Typannotationen.


Wenn aber Tupel-Deklarationen ein Sonderfall des Matching sind, dann sollte man versuchen, die Deklarationsyntax  daran zu orientieren. Vllt. bietet das Int* Konstrukt neue Ideen für das Matching von Mengen! Für Tupel wäre es auf jeden Fall erforderlich..

Friday, December 17, 2010

NeoLinq: Basics

1. Definitionen:


// kommentar => kommentar.
name = Wert     // definiert einen unveränderlichen wert. Der Bezeichner steht also ab jetzt für Wert.
name := Wert     // definiert auch einen wert, aber einen veränderlichen. Es handelt sich also nicht um eine Gleichsetzung,  sondern eine Zuweisung.
fun = {}     // definiert eine parameterlose funktion mit dem rückgabewert ()
argfun = { i :Int -> i + 2 }     // definiert eine funktion mit dem parameter i vom Typ Int und dem Rückgabewert i + 2 vom Ty
val = argfun 3 // val erhält den rückgabe wert von argfun, aufgerufen mit dem parameter 3. das ergebnis: 5.


Beispiele:


wert = 2
inkrement = { i: Int -> i + 1 }
ergebnis := inkrement wert    // 3


Und jetzt mal was krankes mit nem Haufen neuer Syntax:
doppelEffektPlusEins = { (wert: Int, f: {Int -> Int}) ->
   f (f wert) + 1
}


// ergebnis (variable) überschreiben
ergebnis := doppelEffektPlusEins (ergebnis, inkrement)




Das Ganze hat viel mit Smalltalk zu tun:
1. Auswertungsreihenfolge: Starr von links nach rechts. Durch Klammern beeinflussbar
2. Reine Infix Notation. Keine Punkte als Trennzeichen, stattdessen whitespace
3. Operatoren sind einfach Mehodennamen
4. Blöcke, d.h. Funktionen werden als Werte definiert und sind ausführbar. Sie können als echte Closures auf alle Werte der äußeren Scopes zugreifen und diese beeinflusse.
5. Keine Schlüsselwörter. Diese können über Funktionen abgebildet werden.

Funktionale Gedanken:
1. Daten sind immutable. ABER: Imperative Zuweisungen und Definition sind mit der := Syntax, die explizit Varablen deklariert, möglich.
2. Typsystem: Tupel sind echte Datentypen. Typen werde a la Scala annotiert. Typen stehen im Vordergrund, keine Klassen. Vllt sind diese aber nachher dasselbe...

"Mein Zeuch"
1. Es gibt Blöcke mit einem Parameter und ohne. Mehrere Parameter sind nur ein Sonderfall: Hier wird ein Tupel mit festen Typen definiert. Varargs werden ähnlich behandelt werden.
2. Durch Klammern lässt sich die Verarbeitungsreihenfolge beeinflussen. Die Aufgabe ist hier und bei Tupeln aber dieselbe: Klammern vereinigen Werte. Die Kommas bei Tupeln stellen davon nur lediglich einenSsonderfall dar, da hier nicht mehrere Dinge in einen Wert verwandelt werden, sondern mehrere Werte parallel existieren.


Ich weiß noch nicht genau wo meine Ideen hingehen. Es sieht aber doch sehr stark nach OO aus, aber auch mit einer strukturellen Komponente. Wahrscheinlich Scala sehr ähnlich: Strukturelles Typsystem, dass Nominelle Typen (Klassen) ermöglicht und OO-Subtypisierung erlaubt. Und am Ende steht dann irgendwann ein "vereinfachtes" Scala (weil nicht JVM-Kompatibel und es nicht implementiert wird). Wenns gut läuft..

Eine neue Sprache: NeoLinq (Vorläufiger Titel)

Das Ziel: Eine Lernsprache, die neue Möglichkeiten aufzeigen soll, zu Abstrahieren und einfachen Code zu entwickeln, aber dennoch performant ist - einen guten Compiler vorrausgesetzt. Es soll kein neuer Stern am Firmament der Programmierung werden.

Inspirationen
  • Teile von Haskells Typsystem
  • Funktionale Programmierung
  • Lisps Syntax und Grundgedanke, sich auf das Wesentliche zu beschränken: Funktionen.
  • Scalas Konzept, ein reichhaltiges Set an Features bereitzustellen und dem Programmierer die Wahl zu lassen.
Idee:
Der Sprachkern soll NUR die elementaren Bestandteile enthalten und so wenig Konstrukte wie Menschenmöglich beiinhalten OHNE die Ausdruckskraft zu behindern.

Features:
  • Operatoren
    • Unbekannt. Bis auf wenige Ausnahme können allerdings sämtliche US-ASCII-Zeichen als Bezeichner verwendet werden. + ist z.B. ein gültiger Name für eine Methode von Int. Kein Operator! Auch boolschen Operatoren wird es nur als normale Methoden geben. (Teilweise Scala)
    • Keine Operator-Precedence: Folgt daraus. Aller Code wird von links nach rechts ausgewertet. Operatorenreihenfolgen bieten keinen Mehrwert bei der Programmierung. (LISP)
  • Namespaces und Scopes
    • Maximal gibt es zwei Namespaces: Wert und Typ.  Ob es wirklich einen separaten Namespace für Typen gibt, ist noch ungeklärt.
    • Uniform Access Principle:  Methoden und Felder teilen sich den selben Namespace. Es gibt keinen syntaktischen Unterschied zwischen den beiden. (Scala, Eiffel)
    • Method-Overloading / Multimethods: In einem Scope kann es immer nur eine Methode mit einem Namen geben, die gegen genau einen Typen dispatcht wird. Overloading ist nicht erlaubt  (Eiffel)
    • Case-Sensitiv: Horst und horst sind unterschiedliche Bezeichner. In Sprachen mit wenigen Namespaces zwingende Vorraussetzung.
  • Nicht technik-getrieben
    • Es soll kein neues C entwickelt werden - keine Zugeständnisse an die Technik. Dennoch sollte die Sprache nur Konzepte erhalten, für die zumindest eine einfache und performante Umsetzung denkbar ist - wie sie erst mal dahingestellt. Gegenbeispiel: Go.
    • Keine Kompatibilität zu JVM o. CLI: Folgt aus dem ersteren. Falls die Sprache jemals fertig würde, könnte man sich immer noch Gedanken darüber machen, mit welchen Modifikationen sie zu vorhandenen RTEs kompatibel werden kann. Gegenbeispiel: Scala.
  • Keine Schlüsselwörter
    • Die Sprache ist so minimalistisch und generisch wie möglich. Keine Schlüsselwörter, da diese den Compilern die Arbeit erschweren und den Sprachkern weniger generisch machen.
    • Sprachkonstrukte durch Methoden: Soweit möglich sollen alle Sprachkonstrukte durch Methoden abgebildet werden (Lisp).
  • Ideen für Datentypen
    • Technische Basistypen: Int, Float, und wahrscheinlich auch Char und String sowie weitere numerische. To be discussed.
    • Parameterlose Funktion (als kompatibles Gegenstück zum Wert. Nicht als Closure verwendbar).
    • Funktion mit einem Parameter (echte Closure)
    • Gruppierung von Werten durch Tupel und Sequenzen (array-ersatz). Ersetzen mehrere Parameter UND varargs vollständig.
    • Typ. Typen sind selbst Datentypen, die Funktionen wie z.B. Subtypisierung u.ä. anbieten.
    • Typ mit Parameter, a.k.a. Generics. Der Typ kann durch den "Typkonstruktor" in verschiedenen Ausprägungen manifestiert werden. Realisierung offen, könnte sich an Scala orientieren.
    • Unit (=> void). Konstante für das leere Tupel. 
    • Kein null!
Weitere Erklärungen und erste Syntaxideen to come...
Wenn ich mehr gesammelt habe werde ich das Ganze mal zur Diskussion stellen. Vllt entwicklen sich ja in größerer Gemeinschaft ein paar Tolle Ideen.

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).

Sunday, October 31, 2010

Dynamisch Vs. Statisch - Der Krieg der Urgewalten

Dieser Artikel beschäftigt sich mit einem der ältesten Themen der Softwareentwicklung - und einen in dem es die heftigsten Grabenkämpfe gibt: Statische Typprüfung oder doch lieber dynamische Typisierung?

Ziel soll ein Vergleich zwischen den Unterschieden der Systeme sein, nicht der Sprachen und ihrer Features.

Dynamisch typisiert ist nicht gleich dynamisch typisiert, man vergleiche nur PHP, Phyton und Clojure.... Bei den statischen Typsystemen fallen die Unterschiede sogar noch viel größer aus, weil diese viel mehr theoretische Substanz und ein viel aufwendigeres Design für das Typsystem und seine Einbindung enthalten müssen.

Dazu auch gleich der erste Punkt:
Viel Blame an den statischen Typsystemen ist meist eher Blame an einzelnen Sprachen und deren schlechter Umsetzung desselben.

Im Falle Java hat die Unzufriedenheit mit der Sprache soweit geführt, dass die dynamischen Sprachen sogar bei Entwicklern beliebter geworden sind, die früher vllt. statische Typsysteme favorisiert haben. Einer der Hauptgründe für die Unbeliebtheit vieler statischer Sprachen dürfte die fehlende Typinferenz sein, d.h dem Zwang an allen Variablen auch immer die Typen mitanzugeben.

Dies ist jedoch eigentlich nur eine Designschwäche:
Statische Sprachen wie Scala, OCaml oder Haskell, aber auch C# kommen an vielen Fällen ohne Typannotationen aus und verwenden kurze Schlüsselwörter, z.B. var oder let, zur Variablendefinition, ohne Angabe des Typs. Der Code ähnelt dann dem von dynamischen Sprachen und ist ähnlich kurz. Mehr zum Thema Typinferenz in einem späteren Artikel...

Einer der häufig genannten Sellingpoints statischer Sprachen ist nach der Meinung ihrer Evangelisten die Performance, die höhere Sicherheit zur Laufzeit und die bessere Dokumentation des Codes. Außerdem sieht man durch die Typannotationen besser was passiert. Alle diese Punkte sind valide, allerdings zu recht für viele Freunde dynamischer Sprachen nicht stichhaltig genug:
  • Performance ist relativ: Man hat bei dynamischen Sprachen zwangsläufig höhere Kosten durch den dynamischen Dispatch aller Methodenaufrufe. Es gibt hier aber Fortschritte, durch die diese Kosten in einen marginalen Bereich zurückgedrängt werden können. Auch dynamische Sprachen können z.T. den schnelleren statischen Dispatch einsetzen, wenn der Compiler in der Lage ist, die Datentypen aus dem Kontext heraus zu identifizieren(=> Typinferenz). Einige Sprachen, z.B. Clojure, bieten auch die Möglichkeit, Typen explizit zu annotieren und auf diese Art und Weise an Schlüsselstellen statische Performance zu bieten.
  • Typsicherheit: Setzt voraus, dass das Typsystem so umfassend ist dass es möglich ist, damit typsicher zu arbeiten. Das ist aber selten der Fall, eigentlich ist es nur in so mächtigen Typsystemen wie dem von Haskell wirklich möglich. Merke: Jeder Typecast öffnet genau die Lücke, die statische Sprachen eigentlich schließen wollen. Ergo ist Code mit vielen Typecasts ein klarer Sellingpoint für dynamische Sprachen - oder Sprahcen mit besseren Typsystemen. Einen wirklichen Verzicht auf Typecasts wird man wohl nur mit strukturellen Typsystemen erreichen (halt Haskell oder Scala).
  • Bessere Codedokumentation durch Typannotationen: Korrekt. Allerdings sollte die Sprache Typannotationen nur bei Methoden für vorraussetzen, weil nur dort wirklich wichtig ist, welche Typen erwartet werden.
  • Einfacheres Refactoring / Bessere Toolunterstützung: Finde ich sehr wichtig. Statische Sprachen haben es leichter, dem Entwickler Vorschläge zu machen, welche Methoden er aufrufen kann etc. Bei dynamischen Sprachen fehlt oft diese Hilfestellung, weil er Compiler selbst nie alle Funktionen kennen kann, die aufrufbar sind. Dennoch wird es auch hier Fortschritte geben, so dass dieses Argument nur bedingt tauglich ist.
Evangelisten dynamischer Sprachen können außerdem auf klare Vorteile ihres Ansatzes hinweisen:
  • Fehler bei der Typisierung sind eher selten und gehören meist nicht zu den Showstoppern. Korrekt. Wäre dies so, dann gäbe es ja gar keine dynamischen Sprachen. Außerdem bedeutet statische Typsicherheit ja nicht, dass der Code das macht was er soll, sondern nur dass alle Methoden und Typen richtig aufgelöst werden können.
  • Wirklich sicheren Code erreicht man durch Unittests. Auch korrekt, aber man muss theoretisch auch viel mehr testen - nämlich auch dass die Typ-Constraints erfüllt werden. In der Praxis wird man dieses aber nur selten tun. Dennoch: Bei Test-First-Ansätzen ist der Vorteil der Typsicherheit statischer Sprachen zu vernachlässigen, weil der Code hier nur Mittel für einen gesetzen Zweck ist.
  • Keine Typannotationen - Ist wie oben gesagt eine Schwäche vieler statischer Sprachen, aber nicht der statischen Typisierung an sich.
  • Duck-Typing ermöglicht es, Code mit wenig Redundanzen zu schreiben. Der Code legt den Fokus stärker auf Prozesse und Schnittstellen und weniger auf Typhierarchien. Finde ich valide, allerdings ermöglichen strukturelle Typsysteme fast dasselbe. Man muss aber fairerweise sagen, dass man auch nicht alle Spielarten der dynamischen Typisierung durch ein strukturelles Typsystem abbilden kann.

Zwischenstand

Statisch
  • Je stärker das Typsystem, desto mehr machen sich die Vorteile einer statischen Sprache bezahlt. Inbesondere dann wenn nur Single-Dispatch angeboten wird (d.h.: Dispatch auf der Basis von Typen => Vererbung) ist statische Typisierung sehr gut implementierbar.
  • Typinferenz und strukturelle Typsierung sind die Schlüsseleigenschaften zu einfachem Code
  • Objektorientierung und nominelle Typisierung sind nützlich, aber nicht unbedingt erforderlich. Falls aber OO verwendet wird, ist eine klare Objekthierarchie und der Ansatz Everything Is An Object das A und O zur einfacher Verwendung. Nicht nur C++ ist hier ein Gegenbeispiel, sondern auch Java mit seinen Primitiven und der  z.T. etwas eigenwilligen Vererbungshierarchie im JDK.
  • Casts sind der Feind der statischen Typisierung! Wenn häufig Casts nötig werden, oder der Programmierer sie für nötig hält, dann sollte er besser zu einer dynamischen Sprache wechseln. Die Vorteile der statischen Typisierung sind dann nämlich dahin. 
  • Besserer IDE-Support durch das Typsystem möglich
  • Es kann eine bessere Performance erreicht werden
Dynamisch
  • Das Typsystem und seine Struktur treten in den Hintergrund. Der Programmierer kann sich auf Operationen konzentrieren und muss weniger über Typsysteme wissen.
  • Automatische Typkonvertierungen (Koercions) sind in dynamischen Systemen extrem gefährlich. Aufgrund der großzügigeren Typprüfung kann der Code schnell nicht mehr nachvollziehbar werden, wenn zusätzlich noch implizite Konvertierungen vorgenommen werden. Mit einer starken Typisierung ist ein dynamisches Typsystem aber relativ leicht beherrschbar, wie z.B. in Phyton. Gegenbeispiele sind PHP und Javascript.
  • Lispartige Sprachen, d.h. Sprachen die in ihrer eigenen Syntax entwickelt sind, sind fast zwangsläufig von dynamischer Natur. Will man also eine Homophone Sprache verwenden, wird man aus Gründen der Einfachheit immer dynamisch typisieren wollen (Clojure).
  • Der Aufwand für Unittests ist durch die fehlende Sicherheit höher, ebenso die Unterstützung durch IDEs geringer. Der Vorteil, sich stärker auf die Prozesse konzentrieren zu können, ist aber sehr groß.
  • Die Sprache wird einfacher durch die geringe Bedeutung des Typsystems. Flexiblere Ausdrücke sind möglich. Das ermöglicht eine leichtere Implementation und ein schnelleres Verständnis der Sprache. Man vergleiche nur den Umfang eines Clojure-Lernbuchs mit einem Java- oder Scala-Buch...
  • Meiner Meinung nach können nur dynamische Sprachen sinnvollen "Multi-Dispatch" in der Form von Multimethods anbieten.
Und nun? Grundsätzlich kann man es fairerweise so zusammenfassen: Eigentlich gibt es kein schlechter und kein besser. (Ja, ich weiß... laaaangweilige Antwort).

Programmierer, die gut im Kategorisieren und Definieren von Wertmengen sind, werden ihre Produktivität mit statischen Sprachen besser entfalten können. Wenn man eine "perfekte" Lösung erstellt hat, ist meist die Motivation zu Tests geringer - eine wohltypsierte Lösung möchte man wahrscheinlich kaum durch redundante Tests noch mal "beweisen". Da kommt es sicher entgegen, dass die statischen Typsysteme einem diese Aufgabe zu einem gewissen Teil bereits abnehmen.

Programmier, die hingegen prozessorientiert Denken mögen gegenteilige Erfahrungen gemacht haben: Gerade der Ansatz "Test First" ist dann am effektivsten, wenn man mit dynamischen Sprachen arbeitet: Hier werden zuerst die Tests, also die Validierungsroutinen entwickelt und darauf aufbauend eine möglichst einfache Implementation erzeugt, die passt. Sowohl die Definition als auch die Durchführung dieses Vorgangs ist mit dynamischen Sprachen und ihrer stärker auf Ergebnisse ausgelegten "Denkweise" besser möglich. Dafür sind die Ausdrücke dann meist weniger gut in statischen Typsystemen ausdrückbar. Es funktioniert oder es funktioniert nicht - warum ist für das Erreichen eines Ergebnisses ja eigentlich nicht so wichtig.

Ich persönliche tendiere eher zu ersten Sichtweise, halte die zweite aber auch für sehr sinnvoll, und in der Praxis auch leichter anwendbar. Was meiner Meinung nach für beide Sichtweisen gilt ist: Die Sprache muss gut sein.
Das gilt vor allem für die statischen Sprachen! Java, C/C++, ... , alle sind unnötig kompliziert, haben unzureichende Typsysteme mit seltsamen Sonderlocken für Arrays und Strings etc. Da lohnt sich auch für Analytiker meist kaum der Einarbeitungsaufwand. Zum Teil gibt es noch nicht mal die Möglichkeit, Closures zu definieren. Außerdem fehlt vor allem Java und C++ immer noch die Typinferenz, so dass der Code sehr aufwendig zu erstellen und zu pflegen ist.
Vor dem Hintergrund wäre man eigentlich verrückt, die geringen Vorteile einer solchen statischen Sprache den eindeutigen einer dynamischen vorzuziehen. Diese sind nämlich schon allein von ihrem Naturell her einfacher zu entwickeln, da das Typsystem ja egal ist. Abgesehen von hässlichen Kröten wie PHP ist man deshalb auch in der Lage, mit den meisten Sprachen recht ähnliche Ergebnisse zu erreichen und ähnlich zu entwickeln.
Dennoch sehe ich auch in diesem Lager Unterschiede. Insbesondere wundert mich, das viele dynamische Sprachen dennoch objektorientiert sind. Grundsätzlich ist OO ok, aber sie ist im Rahmen von statischen Typsystemen viel nützlicher als in den dynamischeren Umgebungen, denke ich. Dynamische Sprachen sollten sich stärker auf Funktionen als Typen und Klassen konzentrieren!

Meine vollkommen subjektive Meinung
  • Wenn statisch, dann etwas in der Richtung von Scala oder Haskell ("echtes Typsystem", strukturell, Typinferenz). Gerne auch objektorientiert.
  • Wenn dynamisch, dann kann man den Vorteil der Homophonie nutzen: Die Sprache ist in ihrer eigenen Syntax entworfen. Aufbauend auf einer geringen Syntax werden alle Kontrollstrukturen und alle Konstrukte wie z.B. Funktionen ausgedrück, Da bietet sich natürlich die Lisp-Familie mit Clojure an..
  • Ein weiterer sehr starker Contender für dynamische Sprachen ist hier durch seine Ausgereiftheit und sein solides Wachstum Python. Warum? Weil es einfach eine schöne Sprache ist. ;-)

    Polymorphie - Die Vielformigkeit der Vielformigkeit


    Polymorphie("Viele Formen") bedeutet in der Programmierung, dass ein Bezeichner abhängig vom Kontext unterschiedliche Bedeutungen hat. In Typsystemen ist Polymorphie ein sehr wichtiges Konzept um abstrakte Datentypen zu definieren (z.B. die Klassen der Objektorientierung). Nur extrem wenige Sprachen verwenden keinerlei dynamische Polymorphie - und diejenigen, die auf diese Kerneigenschaft verzichten, sind normalerweise.. scheiße. Beispiel: C hat war keine einzige Möglichkeit, durch dynamische Polymorphie Abstraktionen auszudrücken.

    Der Polymorphismus hat ironischerweise selbst auch "viele Formen" und lässt sich in verschiedene Unterkategorien aufteilen. Interessanterweise können sich diese Unterkategorien zum Teil auch ordentlich in die Quere kommen. So sind z.B. das statische Method-Overloading und Vererbung zum Teil inkompatibel. Ich werde im folgenden nicht weiter darauf eingehen, aber vllt. dann mal wenn ich Kovarianz und Kontravarianz im Zusammenhang mit Typparametern (a.k.a. Generics) erkläre.

    1. Ad-Hoc-Polymorphimus (Polymorphismus zur Compilezeit)
    Polymorphimus zur Compilezeit bedeutet, das ad-hoc im Code Polymorphismus eingesetzt wird, um Bezeichner mit Bedeutungen zu überladen, dies aber nicht dynamisch geschieht, sondern durch den Compiler geprüft werden kann.
    • Method Overloading: Eine der bekanntesten Möglichkeiten ist sicherlich die Methodenüberladung. Abhängig von der Parameterliste wird je nach Aufruf eine andere Methode aufgerufen. Dies geschieht aber nicht zur Laufzeit sondern anhand der zur Compilezeit bekannten Typen. Besonders populär in Java, C++, C#.
    • Operator Overloading: Eigentlich nur ein Sonderfall des Method Overloadings. In Sprachen, die es erlauben Operatoren zu überladen, sind Operatoren nämlich eigentlich nur Funktionen, die Symbole wie "+" im Namen verwenden können. Ok, ... außerdem muss noch eine andere Schreibweise für Funktionsaufrufe erlaubt werden: Die Infix-Notation, bei der Parameter Methode Parameter geschrieben wird statt der Präfix-Notation Methode Parameter Parameter
    • Typparameter: Ebenfalls ein Sonderfall des Polymorphimus. Durch Typparameter kann ein Typ, z.B. eine Liste, ad-hoc zur Compilezeit in eine spezialisierte Liste, z.B. für Strings, verwandelt werden. Derselbe Bezeichner "Liste" kann hier so überladen werden, dass er sich im einzelnen Anwendungsfall auf einen oder mehrere bestimmte Typen spezialisiert. Diese werden durch die Typparameter definert. Typparameter sind ein must-have für alle sicheren statischen Typsysteme. (Siehe auch mein anderer Artikel zu Collections.)
    • Implizite Konvertierungen (Type Coercions): Ebenfalls zur Ad-Hoc Polymorphie zählen die impliziten Konvertierungen, wie sie z.B. in vielen Sprachen für numerische Typen gang und gäbe sind. Die Konvertierung ermöglicht es z.B. einen Integer so zu benutzen, als wäre er ein Long-Wert: In noch extremeren Fällen kann es sogar erlaubt sein, einen Integer wie einen String zu benutzen... Auch hier wird also ein Bezeichner mehrfach verwendbar, weil er automatisch in eine passende Rolle "gezwungen" (coerct) wird. Dies ist die einzige Art der Polymorphie, die C beherrscht - und gleichzeitig auch eine sehr fragwürdige, weil sie das Typsystem unsicherer (weak) macht. Typkonvertierungen sind im Allgemeinen Bestandteil der Sprache - es kann aber auch erlaubt sein, dass der Programmierer diese selbst definiert, wie es z.B. in Scala möglich ist.

      2. Dynamischer Polymorphimus (Polymorphismus zur Laufzeit)
      Der dynamische Polymorphismus erlaubt es, erst zur Laufzeit anhand des manifestierten Typs zu entscheiden, was zu tun ist. Insgesamt sind zwei unterschiedliche Strategien bekannt, die als single-dispatch und multi-dispatch bezeichnet werden:
      • Single-Dispatch anhand des Typs (Vererbung, Typklassen)
      Single-Dispatch anhand des Typs ist die verbreitetere Variante. Hierbei ist es möglich, anhand eines abstrakten Typs zu entscheiden, was zu tun ist. Die meisten kennen dies aus der Objektorientierung: Wenn ich auf einer Variablen vom Typ "Set" eine "add"-Methode aufrufe, wird nicht die "add"-Methode von List aufgerufen, sondern von der konkreten Klasse, zu der Wert der Variable gehört. Das kann z:B. ein HashSet oder ein TreeSet sein. Es ist also möglich, durch Abstraktion die Entscheidung auf einen Untertyp zu vertagen, was eigentlich zu ist. Zusätzlich erlaubt es das Konzept der Vererbung, für einige Methoden bereits eine (Default)-Implementierung vorzugeben.
      Auch andere Sprachen, die keine Objektorientierung verwenden, kennen den Single-Dispatch: Haskell verwendet mit seinen Typklassen ein Konzept, das genau das gleiche macht, jedoch auf die starre Objekthierarchie verzichtet. Schaut euch dazu am besten meinen Artikel zu strukturellen Typsystemen an.
      • Multi-Dispatch anhand der Paramaterliste (Multi-Methods)
      Multi-Dispatch ist ein sehr seltenes Phänonem, das eigentlich ausschließlich in dynamischen Sprachen angeboten wird. Da statische Typsysteme sehr stark an Typen und ihren Beziehungen interessiert ist, wird dort traditionell eher der Single-Dispatch verwendet.
      Multi-Dispatch bedeutet dass ich eine Methode zur Laufzeit überladen kann. Dies ist dem Method-Overloading ähnlich, aber flexibler. Wenn ich z.B. eine Multi-Method habe, die drei Parameter entgegen nimmt, dann kann ich dynamisch zur Laufzeit neue Handler definieren, die für eine andere Kombination von Parametertypen gelten. Sobald die Methode mit Werten aufgerufen wird wird dann anhand der Eingangsdatentypen (Klassen) die richtige Methode ausgewählt.
      Das Konstrukt ist deutlich mächtiger als die statische Methodenüberladung und macht primär in dynamischen Sprachen Sinn, die keine Objektorientierung oder Vererbung verwenden (z.B. Clojure).
       
      • Single-Dispatch vs MultiDispatch
      Es ist für eine Sprache sehr unüblich, sowohl single- als auch multidispatch anzubieten, weil beide eine vollkommen andere Herangehensweise haben:
        • Single-Dispatch macht in Systemen Sinn bei denen Typen im Vordergrund stehen, und Abstraktionen anhand der Typschnittstellen realisiert werden. Beispiele: Alle OO-Sprachen, aber auch Haskell.
        • Multi-Dispatch eigent sich für Sprachen, bei denen Operationen im Vordergrund stehen und nicht die Typen. Konsequenterweise wird hier meist auf OO verzichtet und dynamisch typisiert. Beispiele: Lisp-Dialekte, vor allem Clojure.
      Ich werde in meinen Artikel primär den Single-Dispatch betrachten, weil er verbreiteter ist, und ich mich hauptsächlich mit statischen Typsystemen befasse. Für dynamische Sprachen ist Multi-Dispatch oft aber sicher eine bessere Alternative - mir fehlt hierzu nur leider das Wissen und die Praxis.
      • Performance und dynamischer Dispatch
      Das Wort Laufzeit impliziert übrigens auch Kosten: Der dynamische Dispatch ist immer aufwändiger als eine statische Verlinkung von Methoden.
      Bei der Objektorientierung muss z.B. vor jedem virtuellen Methodenaufruf in einer Virtual Method Table nachgeschaut werden, welche Methode denn aufzurufen ist. Diese Kosten können moderne Compiler zu großen Teilen negieren, aber nicht vollständig elimieren. Am effektivsten sind sicherlich Virtual Machines wie die JVM: Diese können zur Laufzeit sicherstellen dass auch wirklich nur die Methoden dynamisch sind, die auch überschrieben werden. In allen anderen Fällen kann die VM den Aufruf zum schnelleren statischem Dispatch optimieren.
      Ad-Hoc-Polymorphismen haben diesen Nachteil nicht: Im Gegenteil können Konstrukte wie Typparameter sogar enorme Performanceverbesserungen bedeuten, da der Compiler den Code mit den präziseren Informationen optimieren kann (aber nicht muss, siehe: Java).


      3. Bewertung der unterschiedlichen Ausprägungen der Polymorphie
      Allgemein kann man sagen, dass Polymorphie gerade für statisch typisierte Sprachen entscheidend sind, um den Code einfacher und generischer zu gestalten. Die wichtigsten Polymorphismen sind definitiv die Typparameter und der dynamische Single-Dispatch.
      Das statische Überladen von Methoden / Operatoren und die impliziten Konvertierungen werden hingegen wohl immer Streitthemen bleiben. Ich würde aber sagen, dass alle ihren Nutzen haben:

      Operatorenüberladung: Alle Sprachen, die Operatoren anbieten, sollten diese auch überladbar machen. Es gibt nichts nervigeres, als wenn die Sprachdesigner meinen, sie wären besser als der Programmierer und dürften Dinge, die er nicht darf. Meist führt dies eher zu schlechterem Code als dies andersherum wäre, weil die eingebauten primitiven Typen dann in Anwendungsfällen vorgezogen werden, in denen man eigentlich selbst passendere Typen definieren sollte.

      Methodenüberladung: Wenn man Operatorenüberladung anbietet sollte man Methodenüberladung im Allgemeinen anbieten. Die Operatorenüberladung ist nämlich eigentlich nur ein Sonderfall der Methodenüberladung.

      Implizite Konvertierungen: Das Streitthema. Implizite Konvertierungen werden meist als unnütz und gefährlich erachtet, und von Freunden der statischen Typisierung als ein Feature von Skriptsprachen verlacht. Das hat eine gewisse Ironie: Tatsächlich bieten meist dynamische "Sprachen" wie PHP exzessive Typumwandlungen an, obwohl diese es gar nicht nötig hätten: Das Duck-Typing macht das Umwandeln von Typen, das man ja primär macht, um Schnittstellen zu erfüllen, nur selten nötig!
      Gerade in objektorientierten Sprachen wären selbstdefinierte implizite Konvertierungen aber sehr nützlich, da hier meist identische Konzepte in unterschiedlichen Objekthierarchien verwendet werden - und die einzige Möglichkeit zum gemeinsamen Verwenden der Hierarchien explizite Typumwandlungen durch Wrapper sind. Implizite Konvertierungen könnten es hier aber ermöglichen, unterschiedliche Hierarchien seamless miteinander zu verbinden!
      Leider ist die einzige Sprache, die bisher diese Möglichkeit bietet, Scala. In allen anderen Sprachen sind die impliziten Konvertierungen schwer verständliche, in die Sprachspezifikation gepresste Features, die nur für bestimmte Datentypen gelten. Allen, die Scalas implizite Konvertierungen noch nicht kennen rate ich, das Thema zu googlen. Es lohnt sich!

      Friday, October 29, 2010

      Strukturelle Typsysteme - Ducktyping Light in statischen Sprachen

      In meinem letzten Beitrag habe ich grob die Typen und Kategorien aufgelistet, die für Typsysteme eine Rolle spielen. Da ich die Kategorien alle nur sehr kurz angeschnitten habe, will ich einige der unbekannteren Aspekte noch einmal detaillierter beschreiben.

      Von allen Kategorien / Bereichen zur Einteilung von Typsystemen sind die strukturellen Typsysteme neben dem Multi-Dispatch wahrscheinlich am wenigsten bekannt. Das ist eigentlich sehr schade, denn sie bilden das fehlende Glied zwischen den aus der Objektorientierung bekannten nominellen und den dynamischen Typsystemen. Doch halt.. Fangen wir von vorne an:

      Die meisten Sprachen mit statischen Typsysteme verwenden ein nominatives Typsystem (auch als nominelles  Typsystem bezeichnet). In nominativen Typsystemen müssen die Typen an allem, was nach außen sichtbar ist (=> Methoden) explizit annotiert werden und schränken hier die möglichen Wertemengen auf den benannten (=> nominellen) Typ ein. Das härteste Beispiel hierfür ist C. Einen etwas flexibleren Weg gehen nominative Typsysteme, die die Subtypisierung erlauben. In der Praxis werden diese Typsysteme auch als "Objektorientiert" bezeichnet. In diesen Typsystemen ist es möglich, nicht nur den Typen selbst an eine Methode zu übergeben, sondern es sind auch Subtypen dieses Typs erlaubt. Soweit wahrscheinlich nichts neues...

      Was aber sind strukturelle Typsysteme? Strukturelle Typsysteme sind relaxter als nominelle Typsysteme bei der Gültigkeit. Sie definieren den Typ nicht anhand des Namens, sondern anhand seiner Schnittstelle. Die Schnittstelle kennen auch nominelle Typsysteme: Sie besagt überlichweise, welche Operationen auf einem Typ möglich sind, z.B. isEmpty(), canJump() etc.
      Der große Unterschied ist nun aber, das bei strukturellen Typsystemen die Beschränkung der Subklassenbildung wegfällt: Ein Wert entspricht dann einem bestimmten Typ, wenn er alle Methoden der Schnittstelle implementiert, unabhängig davon, ob er vom diesem Typ ableitet.

      Beispiel (Scala)

      type Duck = {
        def quack()
        def swim(speed: Int)
      }

      class Swan {
        def quack {
          println("swan")
        }
        def swim(speed: Int){
          println("Is swimming")
        }
      }

      object Main{
        def main(s: Array[String]){
          val duck: Duck = new Swan()
          duck.swim()
        }
      }

      Das Ergebnis: Das Programm wird vom Compiler akzeptiert, obwohl ein Swan nicht von Duck erbt!
      Das ist ja auch vollkommen korrekt, weil der Compiler leicht verifizieren kann, das Swan alle Requirements der Schnittstelle Duck erfüllt und sich damit ordungsgemäß als "Duck" verwenden lassen kann.
      Wie der Name schon andeutet: Ja, es handelt sich hierbei um ein Konstrukt das dem sehr ähnlich ist, das dynamische Sprachen auch als Duck-Typing bezeichnen. Strukturelle Typsysteme ermöglichen es, dieses dynamische Konstrukt in einem statischen Rahmen auszudrücken.

      Strukturelle Typsysteme sind dadurch deutlich mächtiger als nominelle Typsysteme. Insbesondere entfällt das Problem, dass man in einem Fremdsystem normalerweise keine vorhandenen Typen mehr anpassen kann. So ist es z.B. nicht möglich, java.util.List auch in einem anderen Kontext zu benutzen, in dem man eine Schnittstelle hat, die z.B. nur die Methode "isEmpty" verlangt, obwohl der Compilr dieses Konstrukt als sicher identifizieren kann. Warum? Weil man der Klasse java.util.List keine neuen Schnittstellen mehr hinzufügen kann!

      Außerdem gibt es noch einen weiteren Vorteil, den man am besten in Haskell nachvollziehen kann: In einem strukturellen Typsystem ist es theoretisch möglich, ein komplettes Programm typsicher zu schreiben, ohne ein einziges Mal eine Typannotation zu verwenden. Der Compiler ist (fast) immer in der Lage, einen entsprechenden strukturellen Typ ad-hoc festzustellen.

      Beispiel (fiktives Scala)
      def walk(person, steplength, distance){
        val steps = 0
        while (steps < distance){
          person.step(steplength)
          steps += steplength
        }  
      }

      In diesem kleinen Programm kann der Compiler die beiden Typen steplength, und distance relativ leicht anhand des typs step als int identifizieren. Was aber mit Person? Für Person reicht es, wenn er intern einen neuen strukturellen Typ definiert, der wie folgt aufgebaut ist:

      type = {
        def step(steplength: Int)
      }

      Es ist also möglich, Typen implizit durch Typinferenz zu ermitteln und zur Validierung von Methoden zu verwenden! Auf diese Weise kommen strukturelle Typsysteme der Ausdruckskraft der dynamischen Sprachen sehr nahe, haben jedoch nicht deren Unsicherheit! Es können im obigen Beispiel nämlich wirklich nur Werte übergeben werden, die auch die Methodensignatur von "step" aufweisen. Dynamische Sprachen werden sich hingegen erst zur Laufzeit mit einer Exception "beschweren".

      Anmerkung: Die obige Typinferenz an Methoden in Scala ist derzeit übrigens nur fiktiv: Scala verwendet zwar ein strukturelles Typsystem, erzwingt aber stets die Angabe der Typen an Methodensignaturen. Primär geschieht dies wahrscheinlich aus Performancegründen, da strukturelle Typen auf der JVM nur per Reflection umgesetzt werden können.

      Ein häufiger Vorwurf gegenüber struktrullen Typsystemen ist, dass sie unsicherer sind als nominelle Typsysteme, da strukturelle Typen zu "leicht" vertauscht werden können. Diesen Vorwurf halte ich aber nicht für sehr valide, da deratige semantische Typfehler nur sehr selten vorkommen. Und selbst wenn: Nominelle Typsysteme können als eine Unterkategorie der strukturellen Typsysteme verwendet werden. So ist es möglich, in einem grundsätzlich flexiblen strukturellen Typsystem auch die mit härteren und "sicheren" Anforderungen versehenen nonimellen Typsysteme einzusetzen.

      Das populärste Beispiel hierfür ist Scala. Eigentlich verwendet die Sprache ein strukturelles Typsystem. In der Praxis wird man aber immer "objektorientiert" mit nominativen Typen, den Klassen der Objektorientierung, arbeiten. Strukturelle Typen wird man nur dann einsetzen, wenn dies aufgrund fehlender Vererbungsbeziehungen nötig wird.

      Links
      http://en.wikipedia.org/wiki/Structural_type_system

      Übersicht: Eigenschaften von Typsystemen

      Während es beim letzten Mal eher um die Unterscheidung zwischen rein klassenbasierten Typsystemen und dem Zusammenhang von Typparametern und Klassen ging, stehen diesmal ganz generisch die Eigenschaften von Typsystemen im Mittelpunkt.

      Typsysteme... Was genau sind eigentlich Typsysteme? Möglichkeiten zur Einschränkung / Erklärung von Typsystemen gibt es unzählige. Der folgende Artikel soll kurz einige der wichtigsten Eigenschaften aufzeigen. Da Typsysteme die wichtigste Eigenschaft einer Programmiersprache sind bzw. vielleicht sogar die einzige Aufgabe, lohnt es sicherlich, sie etwas genauer zu betrachten.

      Die meisten diese Bereiche werden übrigens in follow-up Artikeln noch etwas genauer beleuchtet werden. Das hier ist nur eine ganz schnöde Auflistung der Features.

      A) Stark vs Schwach
      • Erlaubt das Typsystem implizite Konvertierungen (Type Coercions) von einem Typ in den anderen?
      • Beispieel: 3 + "hallo"; 3 * 3.4. Ist dies möglich?  Welches Ergebnis wird erzeugt?
      • Grundsätzlich ist schwache Typisierung nicht als Nachteil zu sehen. Bei Sprachen die Typen aber zu relaxt handhaben kann es schwieriger werden, richtigen Code zu erzeugen, weil sich schneller Fehler einschleichen können.
      • Eine Besonderheit stellt die Sprache Scala dar: In der Sprache selbst sind keine impliziten Konvertierungen definiert, jedoch ist es möglich, dass der Programmierer diese selbst definiert und explizit in bestimmte Module importiert.

        B) Typenableitung
        • Können neue Typen durch Kombination / "Vererbung" von vorhandenen Typen abgeleitet werden?
        • Populärstes Beispiel: Objektorientierung
        • Ermöglicht fast immer die Single-Dispatch-Polymorphie (s.o.)
        • Kovarianz und Kontravarianz: Spezielle Regeln die für die Parameter von Typen (Generics) und die Parameter von abgeleiteten Methoden gelten.

        C) Typzugehörigkeit: Nominativ < Strukturell < Dynamisch
        • Nominativ
          • Erlauben nur Werte die vom selben Typ (Namen) sind
          • Ohne Typenableitung: Starres und unflexibles Typsystem wie das von C.
          • Mit Typenableitung: Objektorientierung (Java, C++, C#, ...). Hier stellt die erzwungene Beziehung zwischen Objekten immer noch eine sehr starke Einschränkung der möglichen Werte dar.
        • Strukturell
          • Typen definieren mit ein oder mehrere Methoden eine Schnittstelle (ähnlich den Interfaces in Java oder C#)
          • Ein Wert entspricht einem Typ, wenn er alle Methoden implementiert. Dies ist unabhängig davon, ob er ein Subtyp von diesem Typ ist.
          • Deutlich höhere Flexibilität - dem Duck-Typing sehr ähnlich
          • Typen können zu jedem beliebigen Zeitpunkt ad-hoc definiert werden, ohne dass sie in eine Vererbungshierarchie eingebracht werden müssen
          • Nominative Typsysteme können eine Unterklasse der strukturellen Typsysteme bilden. So ist Scala z.B. strukturell typisiert, praktisch wird aber fast immer das nominative objektorientierte System verwendet.
        • Dynamisch (Duck-Typing)
          • Ähnlich der strukturellen Typisierung, es werden aber gar keine Anforderungen mehr an Typen gemacht. Ein Wert muss einfach nur die Methoden unterstützen, die im Code verwendet werden. Tut er dies, läuft der Code. Tut er dies nicht.. bang!
          • Keine Typprüfung mehr durch den Compiler
          • Grundsätzlich etwas schlechtere Performance, die aber kein "showstopper" sein sollte
          • Typische Eigenschaft aller dynamisch typisierten Sprachen, lokal beschränkt aber auch in statischen Sprachen wie C# möglich.
          • In dynamischen Sprachen sind Variablen nicht typisiert, so dass mit einer Zuweisung der Typ einer Variablen wechseln kann. Es ist also möglich in einer Variable zuerst einen String und später einen Int zu halten.
          • Schwach typisierte dynamische Sprachen erzeugen meist schwer nachvollziehbaren Code (PHP). Dies wird meist als Vorwurf gegen alle dynamischen Sprachen benutzt - es gibt aber auch viele stark typisierte Sprachen wie z.B. Phyton auf die dies nicht zutrifft.

        D) Polymorphie
        Polymorphismus ("Viele Formen") bedeutet in der Programmierung, dass ein Bezeichner abhängig vom Kontext unterschiedliche Bedeutungen haben kann.
        • Ad-Hoc-Polymorphie (Compilezeit)
          • Statische Methoden- und Operatorenüberladung
            • Effektiv dasselbe. Operatoren sind eigentlich nur Methoden mit einer anderen Schreibweise.
            • Bezeichnet die Fähigkeit, einem Bezeichner statisch mehrere Methoden mit verschiedenen Parameterlisten zuzuordnen
          • Typparametrisierung (Generics)
            • Ermöglicht ad-hoc Typannotationen an Typen, um spontan aus Templates neue Typen zu bilden. Aus einem Set[T], wobei T die Einfügemarke ist, kann so ein Set[String] oder auch ein Set[Int] werden.
            • Nur für statisch typisierte Sprachen interessant, um Optimierungen vorzunehmen und auf unsichere Casts zu verzichten
            • Zusammen mit der Subtypisierung ergeben sich hier einige sehr komplexe Kombinationsmöglichkeiten durch die Kovarianz und Kontravarianz
        • Dynamische Polymorphie / Dispatch (Laufzeit)
          • Single-Dispatch
            • Subtypenbildung (Typenableitung) ist möglich
            • In der Objektorientierung: Vererbung
            • In der strukturellen Typisierung als Typklassen bezeichnet
          • Multi-Dispatch
            • Dynamische Methodenüberladung zur Laufzeit
            • Anders als beim Single-Dispatch können mehrere Parameter einer Methode überladen sein
            • Auch als Multi-Methods bezeichnet

          E) Typinferenz
          • Hauptsächlich bei statisch typisierten Sprachen wichtig: In welchem Umfang muss der Programmierer Typannotationen bei Variablen und Methodenparametern vornehmen, und inwieweit können diese "wegfallen"?
          • Sprachen, die Typinferenz ermöglichen, machen die Typannotationen stets optional, sie können also verwendet werden, müssen aber nicht. Dadurch wird der Code meist deutlich kürzer und lesbarer.
          • Falls die Sprache Typparameter ermöglicht, können auch meist diese durch die Typinferenz ermittelt werden
          • Arten
            • Nur Rückgabewerte (C, C++, Java, ...)
              • Bei Rückgabewerten ermittelt der Compiler den Typ zur Prüfung
              • Ermöglicht das Method-Chaining: new String("A").concat("B).concat("C")
              • Typannotationen sind stets erforderlich
            • Lokal (Scala, C# 3.0)
              • Type-Inferrence funktioniert nur innerhalb eines begrenzten Scopes, nicht aber auf dem Method-Level. Dadurch müssen alle lokalen Variablen und nicht typisiert werden
              • Bei Methoden müssen die Eingabeparameter, meist aber nicht die Rückgabewerte typisiert werden. Letztere können aus den Rückgabewerten der Methode abgeleitet werden
            • Global (Haskell)
              • Auch Methoden müssen nicht oder nur selten annotiert werden
              • Eine volle Typinferenz erfordert zwingend ein strukturelles Typsystem
              • Gerade bei vollständiger Typinferenz ist es von Vorteil, das Typen immer noch explizit annotiert werden können. Schließlich haben Typannotation an Methoden auch einen Vorteil: Sie dokumentieren, welche Werte erlaubt sind.
          • In den meisten dynamischen Sprachen können Typen gar nicht annotiert werden. Sie haben also automatisch eine Art "vollständiger" Typinferenz ohne Typsicherheit.

          Thursday, October 28, 2010

          Typ > Klasse. Warum Klassen auch in Java nur eine Untermenge der möglichen Typen sind

          In diesem Beitrag geht es primär darum, den nebulösen Begriff des Typsystems etwas genauer zu definieren. Vor allem soll (in einem Follow-Up) aufgezeigt werden, dass das "objektorientierte Paradigma" auch nur ein Sonderfall der etwas generelleren und mächtigeren "strukturellen" Typsysteme ist. Und es soll gezeigt werden, warum Java seit 1.5 zwischen Klassen und Typen unterscheidet!

          Am Anfang war alles ganz einfach. Vielleicht etwas zu einfach: Da Sprachen mit statischen Typsystemen im Vergleich zu den dynamischen fast zwangsläufig dazu tendieren, komplex zu sein, musste eine Vereinfachung her. Also hat man sich beim Design von Java wohl so ungefähr folgendes überlegt:

          Diskussion zwischen James und dem Sonnengott
          J: "Typen sind schon ein ganz schön komplexes Thema. Ich mag Typen. Aber ich will es einfach halten."
          S: "Unser Business-Plan. Wir brauchen Ergebnisse! Lösungen!"
          J: "Jaaa... Moment!"
          S: <Uhrtick>
          J: "Ok, wie wärs damit: Typ == Klasse! Die einzig zulässigen Typen sind Klassen!"
          S: "Gut. Die Propaganda ist in schon in Arbeit. Typ == Klasse. Das wird das Fundament der ganzen OO werden!"
          J: "Und wenn wir uns irren? Oh.. Und ich hab scheiße gebaut. Was ist mit den Primitiven? Und den Arrays?"
          S: "Klappe!"

          So oder so ähnlich muss es wohl ergegangen sein. Egal. Worum es mir heute geht ist nur folgendes:
          Typen sind keine Klassen... aber Klassen sind Typen.

          Fangen wir wie folgt an: Und zwar mit einer rudimentären Definition:

          Typ = Einschränkung der möglichen Werte für einen Bezeichner (üblicherweise eine Variable oder ein Funktionsparameter). Typen stehen bei den statischen Typsystemen im Mittelpunkt, die versuchen durch eine Validierung der Typen möglichst viele Fehler von vornherein durch einen Compiler zu verhindern.

          Klassentyp = Einschränkung der möglichen Werte für einen Bezeichner auf einen bestimmten Typ, der von der angebenen Klasse oder einer von ihr abgeleiteten Klassen sein muss. Offizielle Bezeichung: Nomineller Typ mit Subtypisierung.


          Mögliche Typen in Java

          Der Klassentyp ist nur ein Sonderfall des generischen "Typs". In Java war die Klasse in Java vor 1.5 der einzig zugelassene Typ - Arrays / Primitive sind Fehler im Sprachdesign, die ich hier ignoriere. Bis heute ist sie traditionell auch der am häufigsten anzutreffende Typ. Durch die Generics mag sich dies grundlegend geändert haben. Aber das interessante ist: Selbst in Java < 1.5 gibt es schon einen Unterschied!

          Beispiel 1: Subklassenbildung

          List list = new ArrayList();
          Bezeichner: list;      Klassentyp: List;       Klasse: ArrayList;    Wert: []

          Das Bezeichner mit einem Obertyp auf Objekte von einer Subklasse verweisen können, ist natürlich in der Objektorientierung ein alter Hut. Aber es ist eine sehr wichtige Erkenntis! Wichtig deshalb, weil durch das obige Konstrukt das Typsystem weniger weiß als es wissen müsste, da ich seine Wertebereich bewusst stärker eingeschränkt habe, als es nötig gewesen wäre. Ich hätte ja auch einfach das folgende schreiben können: ArrayList list = new ArrayList(). Dafür könnte ich dann auch keine LinkedListen mehr für die list-Variable verwenden.

          Beispiel 2: Typparameter

          List<String> list = new ArrayList<String>();
          Bezeichner: list;      Klassentyp: List<String>;       Klasse: ArrayList;    Wert: []

          Was dieses Beispiel so interessant macht, ist das hier am deutlichsten wird, das Generics in Java nur für das Typsystem interessant sind und das Typen mehr als nur Klassen sind. Ich erschaffe hier zur Laufzeit anhand der Typparameter einen neuen Typ - List<String>. Die Klasse hingegen hat sich dadurch nicht geändert, sie weiß nicht einmal etwas über den Typ. Im besten Fall könnte ich in einer Sprache ohne erasure noch wissen dass ich hier eine Klasse mit einem Typ String (nicht: Klasse String) instanziert habe.
          In Sprachen wie Java < 1.5 ohne Typparameter, in denen Typen und Klassen immer dasselbe sind, lässt mich das Typsystem an der wichtigsten Stelle hängen: Reine Klassentypen sind nicht mächtig genug um damit alle wichtigen Typen abbilden zu können. Praktisch bedeutet dies, dass ich in diesen Sprachen sehr oft mit "up-casts" arbeiten muss und damit alle meine Datenstrukturen nicht typsicher sind.

          Beispiel 3: Typparameter können mehr sein als nur Klassen

          Java nennt sie Generics, aber dennoch ist die Bezeichnung Typparameter die eigentlich wichtige. Die meisten können sicher verstehen, das Typparameter wie der Name sagt, Typen, keine Klassen erwarten. Ich will es dennoch kurz erklären:

          Map<String,Set<Integer>> map = new HashMap<String,Set<Integer>>();

          Alles klar? Wenn ich nur Klassen benutzen dürfte, wäre die Bezeichnung illegal, da Set<Integer> ein Typ ist, die Klasse hingegen nur Set. Das wäre aber ziemlicher Dreck und wird deshalb nicht gemacht. Merke: Es heißen Typparameter, nicht Klassenparameter. Und: Sie sind der Hauptgrund dafür, dass Typen und Klassen sich seit Java 1.5 unterscheiden.

          Beispiel 4: Typen können Klassen kombinieren

          Bis hierhin mag noch alles lustig gewesen sein, doch jetzt kommt etwas dass die meisten wahrscheinlich noch nicht wissen: Typen können sich sogar aus mehreren Klassen zusammensetzen (Kombinierte Typen):

          interface X{

            <A extends Set<?> & Comparable<Set>> void action(A a);

          }

          Diese Methode erwartet als Eingabeparameter eine Variable von einem Typ, der sich aus den Typen Set und Comparable zusammensetzt. Hier wird ein neuer Typ definiert, der sich aus vorhandenen zusammensetzt. Dies ist ähnlich den normalen Typparametern, die die das definieren von "Ad-hoc" Typen aus existierenden Typen ermöglichen. Aber hiermit ist eine noch größere Flexibilität gegeben: Der kombinierte Typ enthält alle Methoden des Comparable UND des Set interfaces, stellt also quasi eine neue Schnittstelle dar OHNE dass vorher eine explizite Vererbungsbeziehung definiert werden müsste.

          An dieser Stelle soll nicht über Sinn und Zweck von aus Klassen zusammengesetzten Typen gesprochen werden - diese sind meist eher bescheiden. Viel interessanter ist aber, das Java hier ein Feature hat, das Typen mächtiger macht als Klassen. Wie so oft bei Java ist hier ein mächtiges Feature, flexible Typendefinition ohne direkte Vererbung, vorhanden, aber so verkrüppelt und schwach dass es meist nur begrenzten Sinn macht.

          Für alle die sich fragen was ich meine: Java basiert seit 1.5 fast auf einem strukturellen Typsystem. Die hier genannten Veränderungen mit den zusammengesetzten Typen haben nichts mehr mit den bisher bekannten starren Typhierarchien der Objektorientierung zu tun. Aber: Java geht den entscheidenden Schritt nicht: Es bleibt ein starres System mit schlechter Erweiterbarkeit. Es unterstützt nicht die flexible Definition von Typen, die überhaupt nicht mehr auf Klassen basieren, sondern erlaubt es nur, vorhandene Klassen zu kombinieren.

          Ich werde im nächsten Artikel genauer auf das Thema der Typsysteme eingehen. Im Mittelpunkt wird dabei der Vergleich der statischen nominellen Typsysteme (C), der nominellen Typsysteme mit Subtypen (Java, C++) und der strukturellen Typsysteme (Haskell, Scala) stehen.