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.