Suche
5 Engel für Gopher, oder: Warum Gopher von Natur aus lazy sind

5 Dinge, die man an Go hassen kann (aber nicht muss)

Christoph Engelbert

© Reneé French (CC BY 3.0 DE / modifiziert)

Unter den Neuankömmlingen der Programmiersprachen rückt Go(lang) in den letzten Monaten und Jahren immer stärker in den Vordergrund. Neuankömmling ist dabei etwas hoch gegriffen, denn tatsächlich wurde Go bereits 2009 von Google vorgestellt und ist seit 2012 als stabile Version (damals 1.0) erhältlich. In der Zwischenzeit hat es Go auf eine stattliche Version 1.10 gebracht und dabei eine Menge an Funktionalitäten gewonnen.

Achso, deshalb heißt es eGOistisch!

Dass Go gerne eigene Wege beschreitet, wenn es um die Implementierung oder Syntax geht, ist mittlerweile weithin bekannt. In Englisch wird es dabei oft liebevoll als „opinionated“ umschrieben. Das Problem dabei ist, dass viele Konstrukte, die aus anderen Programmiersprachen bekannt sind, bei Go in dieser Form schlichtweg nicht existieren oder sich gänzlich anders verhalten als gewohnt. Gerade Letzteres führt dabei zu unerwarteten Fehlern und mitunter auch zu Unverständnis auf Entwicklerseite.

Die strikten Syntaxeigenschaften von Go dagegen erzeugen oftmals nur ein müdes Lächeln. Ungenutzte Importe und Variablen sind vom Go Compiler nicht gerne gesehen und werden ebenso vehement abgeschmettert. Auch eine öffnende, geschweifte Klammer, welche unverfroren in eine neue Zeile gesetzt wird, ist ungeliebt. Generell zwingt Go einen zu einem relativ statischen, dafür aber durchaus (nahezu) einheitlichen Programmierstil. Was der Go Compiler nicht mag, läuft ganz sachlich auf eines hinaus: Compile Error.

Dafür bietet Go strikte Typsicherheit. Teils so strikt, dass wir damit besondere Effekte und Programmierfehler erzeugen können, wie wir im Laufe dieses Artikel sehen werden. Trotzdem muss man in Go nur selten Typen explizit ausschreiben, da diese im Normalfall aus der Wertezuweisung abgeleitet werden (Typinferenz / Type Inference).

I don’t give a FAQ!

Ich habe vor etwas über einem Jahr angefangen, mich intensiver mit Go zu beschäftigen und damit zu entwickeln. Dabei ist Go nicht unbedingt meine Lieblingssprache geworden, aber ich gestehe ein, dass man durchaus produktiv und fix damit arbeiten kann. Tatsächlich habe ich bereits mehrere kleine Dinge damit gebaut. Hauptsächlich handelt es sich an der Stelle um Anwendungen für Embedded-Systeme. Die Cross-Compiler-Fähigkeiten (also das Kompilieren für ein anderes Betriebssystem oder eine andere CPU-Plattform) der Go Toolchain sind dabei fantastisch und weit vor dem Wettbewerb.

Doch zurück zu den Eigenheiten, die Go so besonders machen. Während der Einstieg bei Go recht leicht ist und gerade mal ein Wochenende vergeht, bis man die Basics verstanden hat, treten die seltsamen Fälle meistens erst dann auf, wenn man sich an etwas Größeres setzt.

Teilweise sind diese Eigenheiten dabei so seltsam, dass Google für Go eine FAQ im Sinne von „warum verhält sich X so und so?“ aufgesetzt hat. Nein, kein Scherz. Die Sprache macht viele Dinge so anders, dass gefühlt jeder Programmierer über diese Dinge stolpert. Dies wird im Übrigen durch den Gopher-Slack-Kanal mit Aussagen wie „dann hast du Go jetzt richtig kennengelernt, die Frage stellt jeder Entwickler im Laufe seiner Go-Laufbahn“ bestätigt. Intuitiv passt also oft nicht mit den Eigenheiten von Go zusammen. So werden bei Googles C-Alternative z.B. öffentliche Typen, Funktionen, Konstanten und andere Konstrukte dadurch öffentlich, dass sie mit einem Großbuchstaben beginnen. Ein Kleinbuchstabe am Anfang des Identifiers endet in einem privaten Zugriffsmodifizierer.

Man muss Go dabei zugutehalten, dass viele der Entscheidungen durchaus langwierige Diskussionen auf den Mailing-Listen oder in den Proposal-Dokumenten hinter sich haben. Nachlesen dieser bringt bei den meisten Punkten einen Aha-Effekt zum Vorschein. Leider sind die Anwendungsfälle hinter den Erklärungen so speziell, dass für viele Entwickler trotzdem noch unklar sein wird, was das genau mit ihrem Problem zu tun hat.

Mein persönlicher Favorit dabei ist, dass Go keinen Reentrant Lock bietet, also einen Lock der vom selben Thread oder derselben Goroutine (die Go-Variante von Coroutinen oder Green Threads) rekursiv akquiriert werden kann. Ohne Hacks besteht allerdings auch keine Möglichkeit, sich eine solche Implementierung selber zu bauen, da Threads in Go nicht zur Verfügung stehen und Goroutines keinen Identifier bieten, womit man dieselbe Goroutine wiedererkennen könnte.

Aber gut, das geht schon sehr tief in die Materie. In diesem Artikel will ich nun fünf Eigenschaften von Go und seiner Syntax vorstellen, die nicht so offensichtlich sind, wie eine fehlende Klasse.


Du hast nen Schatten

Fangen wir mit etwas Einfachem an: Jeder gute Entwickler hat schon einmal von Shadowing gehört. Meistens geschieht dies im Zusammenhang mit Variablen. Ein einfaches Beispiel braucht nur zwei Scopes.

foo(“foo”)
func foo(var1 string) {
  for {
    var1 := “bar”
    fmt.Println(var1)
    break
  }
}

Als kurze Einführung für Nicht-Gopher: die Zuweisung per := erzeugt eine neue Variable und identifiziert den Typen der Variable anhand des zugewiesenen Wertes (Typinferenz), in diesem Fall ist dies ein String. Wir erzeugen also im inneren Scope (die for-Schleife) eine Variable, die denselben Namen wie der Funktionsparameter trägt. Wir überlagern (Shadowing) den Parameter und unsere Funktion gibt bar aus.

So weit so gut. In Go ist es aber nun notwendig, bei Eigenschaften von anderen Packages (also Structs, Methoden, Funktionen, …) den Package-Namen mit anzugeben. Zu sehen am Package fmt, das die Funktion Println zur Verfügung stellt.

Bauen wir also unser Beispiel ein wenig um:

foo(“foo”)
func foo(var1 string) {
  for {
    fmt := “bar”
    fmt.Println(var1)
    break
  }
}

Was wir nun bekommen ist ein Compiler-Fehler. Ist ja auch klar, immerhin versuchen wir jetzt die Funktion Println auf einem String aufzurufen. Leider ist das nicht immer ganz so offensichtlich wie hier. Schon wenige Zeilen Abstand können hier wahre Freude aufkommen lassen, wenn urplötzlich der Code nicht mehr kompiliert.

Richtig ungemütlich wird es dann, wenn ein Struct überlagert wird. Bauen wir uns als Beispiel mal ein etwas seltsames Design, wir sind ja gute Entwickler:

type task struct {
}

func main() {
  task := &task{}
}

Wir bauen uns also ein Struct namens task und erzeugen davon eine Instanz. Der kleingeschriebene task für den Struct-Namen ist dabei Absicht, da Go, wie bereits erwähnt, anhand des ersten Buchstabens die Sichtbarkeit festlegt, hier also privat.

Soweit sieht es gut aus und Go kompiliert unseren liebevoll erstellten Task. Unerwartet wird es nun, wenn wir eine Zeile hinzufügen.

type task struct {
}

func main() {
  task := &task{}
  task = &task{}
}

Ein erneuter Kompiliervorgang schlägt nun mit der Fehlermeldung task is not a type (task ist kein Typ) fehl. An dieser Stelle ist Go also nicht mehr in der Lage, die Unterscheidung zwischen Typ und Variable zu sehen. Eigentlich logisch, aber es hat ja gerade noch geklappt. Während man in JavaScript noch begründen könnte, dass die Variable task nun eine Referenz auf einen Typ sein könnte, ist dies in Go nicht möglich, da einer Variable kein Typ als Wert zugewiesen werden kann.

Stellt sich nun die Frage: Ist dies tragisch oder nicht? Generell nicht, dennoch ist es mir schon öfter passiert, dass man ohne darüber nachzudenken, einer Variable einen Struct-Namen gegeben hat und ein paar Zeilen darunter verzweifelt versucht, auf ein gleichnamiges Struct oder Package zuzugreifen. Es kostet halt jedes Mal ein paar Minuten Zeit, bis es wieder Klick macht.

Da wir aber schon von Typen reden, gleich auf zum Nächsten.

Typ oder nicht Typ, das ist hier die Frage!

Wir haben jetzt schon gesehen, wie Structs und Funktionen erzeugt werden. Wäre es nicht aber auch hin und wieder schön, einen Typen einmal „umzubenennen“?

type handle int

Damit erzeugen wir einen Typen namens handle, der sich wie int verhält. Generell ist ein solches Feature als Type Alias bekannt, denkt man – nicht so in Go. Spätestens seit Go 1.9 ist dies sonnenklar.

Schauen wir uns an, was wir mit Go Schönes machen können:

type handle int

func main() {
  var var1 int = 1
  var var2 handle = 2
  types(var1)
  types(var2)
}

func types(val interface{}) {
  switch v := val.(type) {
  case int:
    fmt.Println(fmt.Sprintf("I am an int: %d", v))
  case handle:
    fmt.Println(fmt.Sprintf("I am an handle: %d", v))
  }
}
I am an int: 1
I am an handle: 2

In diesem Beispiel benutzen wir gleich mehrere, eigentlich ganze coole Features von Go. Das Statement switch-type-case ist dabei eine Art Pattern Matching auf den Typen und wirkt ähnlich wie ein instanceof oder typeof in Java bzw. JavaScript. Das interface{} nehmen wir vereinfacht als Äquivalent zum Object in Java, denn es handelt sich hierbei „nur“ um ein leeres Interface, das jeder Go-Typ automatisch implementiert.

Das Interessante ist nun, dass man als Java-Entwickler erwarten würde, dass ein handle durchaus auch ein int ist. Damit müsste der erste Fall eintreten. Leider ist dies nicht der Fall, da Go keine Vererbung im klassischen, objektorientierten Sinne kennt.

Lesen Sie auch: Go 1.10 bringt viele Verbesserungen für Packages, Tools und Laufzeitsysteme

Die alternative Erwartung wäre, dass handle ein Alias für int ist, wie z.B. ein typedef in C/C++, doch auch das ist nicht der Fall. Der Go Compiler erzeugt hier eine neue TypeSpec, quasi eine Kopie des originalen Typen. Dennoch sind diese vollkommen unabhängig voneinander.

Seit Go 1.9 gibt es nun allerdings auch ein echtes Type Alias. Das nachfolgende Beispiel ist nur minimal abgewandelt, um den Code wieder kompilierbar zu machen.

type handle = int

func main() {
  var var1 int = 1
  var var2 handle = 2
  types(var1)
  types(var2)
}

func types(val interface{}) {
  switch v := val.(type) {
  case int:
    fmt.Println(fmt.Sprintf("I am an int: %d", v))
  }
  switch v := val.(type) {
  case handle:
    fmt.Println(fmt.Sprintf("I am an handle: %d", v))
  }
}
I am an int: 1
I am an int: 2
I am an handle: 1
I am an handle: 2

Unterschied aufgefallen? Genau, statt type handle int nutzen wir nun type handle = int und erzeugen so einen zusätzlichen Namen für int, nämlich handle. Damit muss dann auch das switch-Statement angepasst werden, da int und handle in diesem Fall für den Compiler derselbe Typ sind, sonst hat man einen doppelten Case und damit wieder einen Compiler-Fehler. Da es das Type Alias erst seit Go 1.9 gibt, dachten viele, dass das zuoberst beschriebene Type Cloning ein Type Alias wäre. Die damit auftretenden Probleme ergaben auf den ersten Blick nicht immer Sinn.

Definieren wir uns also einen Typen namens Callable, welcher aus einer einfachen Funktion ohne Parameter oder Rückgabewert besteht.

type Callable func()

Nun wollen eine entsprechende Funktion erzeugen.

func main() {
  myCallable := func() {
    fmt.Println(“callable”)
  }
  test(myCallable)
}

func test(callable Callable) {
  callable()
}

Easy. Dank der Typinferenz von Go erkennt der Compiler automatisch, dass myCallable der Funktionssignatur von Callable entspricht. Dadurch kann der Compiler myCallable implizit auf ein Callable casten. Im Anschluss kann myCallable der Funktion test übergeben werden. Dies ist übrigens eine der wenigen Ausnahmen, wo ein impliziter Cast durchgeführt wird, normalerweise müssen alle Formen von Casts explizit ausgeschrieben werden.

Unerwartet wird es nun, wenn Reflection ins Spiel kommt. Reflection bietet, wie in anderen Sprachen auch, Funktionalitäten, um zur Laufzeit das Verhalten zu analysieren oder zu verändern. Dazu nutzt man oftmals Typinformationen um das Laufzeitverhalten entsprechend des Datentyps zu verändern.

type Callable func()

func main() {
  callable1 := func() {
    fmt.Println("callable1")
  }

  var callable2 Callable
  callable2 = func() {
    fmt.Println("callable2")
  }

  test(callable1)
  test(callable2)
}

func test(val interface{}) {
  switch v := val.(type) {
  case func():
    v()
  default:
    fmt.Println(“wrong type”)
  }
}
callable1
wrong type

Hier hat callable1 nun den Funktionstypen func(), während callable2 explizit ein Callable ist. Callable ist dabei eine eigene TypeSpec und damit nicht mehr derselbe Typ wie func(). Per Reflection müssen nun beide Fälle einzeln abgefangen werden. Fixen lässt sich das Ganze wieder mit der seit Go 1.9 existierenden Möglichkeit des Type Alias.

type Callable = func()

Hier ist das Suchen des Fehlers ein reiner Debugging-Spaß, immerhin gibt es nicht einmal eine Fehlermeldung, es funktioniert einfach nicht.

Gopher sind von Natur aus lazy!

Einer meiner Lieblinge, und der erste Eintrag dieser Liste der auch in den FAQ zu finden ist, ist die Lazy Evaluation, also das verzögerte Ausführen von Code. Java Entwicklern ist dies seit Einführung des Stream APIs bestens bekannt.

Schauen wir uns den folgenden Code-Schnipsel einmal an:

func main() {
  functions := make([]func(), 3)
    for i := 0; i < 3; i++ {
      functions[i] = func() {
      fmt.Println(fmt.Sprintf("iterator value: %d", i))
      }
    }

  functions[0]()
  functions[1]()
  functions[2]()
}

Keine große Magie. Ein Array mit drei Elementen, ein Loop und Closures, die die Variable i einfangen. Was kommt aber raus?

iterator value: 3
iterator value: 3
iterator value: 3

Natürlich 0, 1, 2! Moment, das ist ja 3, 3, 3. Richtig! Willkommen im „Was, das geht?”-Land, wie ein großer, deutscher Kabelanbieter so schön sagt.

Andere Programmiersprachen wie etwa Java fangen bei der Erzeugung eines Closures den Wert der Variable, in Go hingegen wird nur ein Pointer auf die Variable festgehalten. Während der Iterationen verändert sich nun der Wert der Variable aber fortlaufend. Nach Abschluss der Schleife führen wir die Closures aus und sehen immer nur den letzten Wert, was etwas unerwartet ist. Mit dem Wissen, dass wir nur den Pointer haben, ergibt das Verhalten dann plötzlich aber doch Sinn. Nur intuitiv ist es wieder einmal nicht.

Da wir aber den Wert festhalten wollen, müssen wir nun herausfinden, wie wir den Wert beim Erzeugen des Closure evaluieren lassen können. Natürlich ist die Syntax dafür ein künstlerisches Meisterstück.

func main() {
  functions := make([]func(), 3)
  for i := 0; i < 3; i++ {
    functions[i] = func(y int) func() {
      return func() {
        fmt.Println(fmt.Sprintf("iterator value: %d", y))
      }
    }(i)
  }

  functions[0]()
  functions[1]()
  functions[2]()
}

Wir erzeugen uns eine temporäre Funktion, die als Parameter die Variable aufnimmt und als Rückgabewert unser Closure liefert. Diese rufen wir sofort auf. Da der Wert der Variable beim Aufruf der äußeren Funktion evaluiert werden muss, fängt unser inneres Closure nun den richtigen Wert. Wir bekommen 0, 1, 2.

Kurz vor Fertigstellung des Artikels, habe ich dann noch eine weitere Alternative kennengelernt. Dazu erzeugt man in der Schleife eine Variable mit dem gleichen Namen und weist dieser den eigentlichen Wert zu. Auch damit wird der Wert der Variable gefangen, da durch diese Methode in jedem Schleifendurchlauf eine neue Variable (und damit ein neuer Pointer) erzeugt wird.

func main() {
  functions := make([]func(), 3)
  for i := 0; i < 3; i++ {
    i := i // Trick mit neuer Variable
    functions[i] = func() {
      fmt.Println(fmt.Sprintf("iterator value: %d", i))
    }
  }

  functions[0]()
  functions[1]()
  functions[2]()
}

Generell ist Lazy Evaluation aus Sicht der Ausführungsgeschwindigkeit durchaus ein interessantes Thema. Immerhin könnte ich die Closures ja erstellen, ohne sie jemals zu nutzen. Wieso also evaluieren? Dennoch ist dies meine Nummer 1 in Sachen „kontraintuitiv“.

Sind wir nicht alle ein bisschen Gopher?

Erinnern wir uns daran, was wir bereits gelernt haben: interface{} in Go ist sowas wie Object in Java – ein leeres Interface, das jeder Typ in Go automatisch implementiert. Automatisch implementieren gilt dabei nicht nur für ein leeres Interface. Jedes Struct, das alle Methoden eines beliebigen Interfaces implementiert, erfüllt automatisch dieses Interface.

Zur Verdeutlichung stellen wir uns einmal folgendes Beispiel vor:

type Sortable interface {
  Sort(other Sortable)
}

Ein Struct, das diese Methode definiert, ist damit also automatisch auch ein Sortable.

type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}

Abgesehen von der Syntax des Receiver Type, von der wir uns nicht abschrecken lassen und die genutzt wird, um eine Funktion an einen Typen zu binden (in diesem Fall an ein Struct), haben wir alle Methoden des Interfaces Sortable implementiert. Wir sind ein Sortable!

var sortable Sortable = &MyStruct{}

Auch wenn automatisch implementierte Interfaces auf den ersten Blick durchaus sinnvoll erscheinen, wird dies spätestens bei größeren Anwendungen, wo mehrere Interfaces die gleichen Methoden aufweisen, kompliziert und vor allem undurchsichtig. Welches Interface wollte der Entwickler eigentlich implementieren? Hoffentlich hat er Kommentare im Code hinterlassen! Weitere, lustige Nebeneffekte durch das Typensystem und das automatische (oder eben nicht automatische) Implementieren von Interfaces könnt ihr hier nachlesen.

Auch zur Sicherstellung, dass ein Typ ein Interface implementiert, hat Go eine Lösung parat, ein implements wie in Java wäre schließlich zu einfach.

type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}
var _ Sortable = MyStruct{}
var _ Sortable = (*MyStruct)(nil)

Ihr seht schon, großartig. Auch hier kann ich nur noch einmal die künstlerische Ausgestaltung des Source Codes von alltäglichen Problem ansprechen. Danke, Go!

Von Nil und Nichtig

Aber wir sind ja noch nicht fertig. Einen haben wir noch!

Das Null oder Nil der Milliarden-Dollar-Fehler ist, dürfte hinlänglich bekannt sein. Dass Nichts aber nicht immer Nichts ist, vermutlich noch nicht. Doch Go belehrt uns eines Besseren! Dazu legen wir uns zuerst einen eigenen Error-Typ (Exception) an.

type MyError string
func (m MyError) Error() string {
  return string(m)
}

In diesem Fall erzeugen wir einen neuen Typ, der vom Typen string geklont wird. Wir wollen nur eine Nachricht für den Fehler, also passt das. Um das Interface error (ja, kleingeschrieben, theoretisch dürfte das nicht öffentlich sein, aber Go kann alles) zu implementieren, muss noch die Methode Error her.

Als nächstes brauchen wir noch eine Funktion, die uns immer Nil zurückgibt.

func test(v bool) error {
  var e *MyError = nil
  if v {
    return nil
  }
  return e
}

Diese Funktion gibt nil zurück, egal ob wir true oder false in die Funktion übergeben, oder doch nicht?

func main() {
  fmt.Println(nil == test(true))
  fmt.Println(nil == test(false))
}
true
false

Beim Zurückgeben von e wird aus dem *MyError-Pointer eine Instanz vom Interface-Typen error und der ist nicht nil! Nicht logisch? Doch logisch, wenn man weiß, wie Interfaces, und ein solches ist error, in Go dargestellt werden.

Intern ist ein Interface nämlich ein Struct aus der eigentlichen Zielinstanz (hier nil) und dem Typ (hier error) und laut Go-Spezifikation ist eine Interface-Instanz nur dann nil, wenn beide Werte dieses Structs nil sind. Daher immer nil explizit zurückgeben, wenn man nil meint! Nachzulesen wieder in der FAQ.


Sondergopher

Zum guten Abschluss gehört noch ein zusätzlicher Punkt, der nicht unerwähnt bleiben sollte. Wie bereits kurz angerissen, bezieht Go seine Visibility für Typen und Funktionen anhand des zugewiesenen Namens. Ist der erste Buchstabe ein Großbuchstabe (Foo), ist die Funktion oder der Typ öffentlich (public), ist der erste Buchstabe ein Kleinbuchstabe (foo), ist das Resultat privat. Dabei kennt Go für private eigentlich nur package-private, wenn man Java-Terminologie annimmt.

Ok, so weit so gruselig, aber lassen wir den persönlichen Geschmack außen vor. Im Allgemeinen kommt man mit dieser Visibility-Regelung durchaus gut zurecht, wenn man davon absieht, dass in Go alles CamelCase (Binnenmajuskel) nutzt, egal ob Funktion, Struct, Konstante, oder oder oder. Aber wir haben ja alle IDEs mit Syntax Highlighting!

Interessant ist nur, dass Go Unicode Identifier akzeptiert. Ergo ist 日本語 (Nihongo = Japanisch) ein legaler Identifier, aber eben immer private. Es gibt eben keine Großbuchstaben für japanische Zeichen.

Lesen Sie auch: Google Go für Java-Entwickler: Alles was Sie wissen müssen

Der GOzilla lässt grüßen

Go ist an einigen Stellen eine sehr eigenartige Sprache. In der täglichen Arbeit kann man durchaus viel Spaß mit Go haben und wenn man die genannten Fallstricke (plus ein paar weitere) kennt, laufen auch größere Programme rund. Trotzdem wird man immer wieder daran erinnert, dass irgendetwas mit der Sprache nicht stimmt. Irgendwie fühlt es sich einfach nicht nach zu Hause an und wir wissen ja alle: “There’s no place like home”.

In den letzten Jahren hat sich bei Go viel getan. Neue Funktionalitäten sind hinzugekommen und viele Punkte die verbessert werden sollen, stehen auf der Liste für Go 2. Darunter einige Ungereimtheiten in der Syntax aber auch im Ablaufverhalten der Runtime. Wann Go 2 kommt, steht dagegen in den Sternen. Eine Roadmap ist nicht verfügbar.

Wer heute mit Go arbeiten will, dem kann ich das (trotz der Gruselgeschichten) nur empfehlen. Man muss sich lediglich darauf gefasst machen, dass öfter mal verdutzte Blicke, eine (längere) Debugging-Session und eventuell ein Besuch der FAQ oder des Gopher Slacks ansteht. Abgesehen davon:

Happy Gophing!

Verwandte Themen:

Geschrieben von
Christoph Engelbert
Christoph Engelbert
Christoph Engelbert ist Open-Source-Enthusiast und immer am "Java-Next" interessiert. In seiner Freizeit kämpft er mit Performance-Optimierungen, den JVM-Interna oder dem Garbage Collector, wobei er als freier Consultant zu diesen Themen auch Rede und Antwort steht. Weiterhin glaubt er fest an Polyglot und ist im Umfeld von Java, Kotlin, Go, TypeScript und anderen Sprachen unterwegs.
Kommentare
  1. Marc2018-04-15 10:32:58

    Ich würde es vielleicht nicht unbedingt Gruselgeschichten nennen - Einige Dinge sind halt eben nicht Java aber das macht sie nicht schlechter.

    Bsp. implizite Interfaces: Man kann, wo immer man es benötigt, ein Interface definieren, das exakt auf den jeweiligen Anwendungsfall zugeschnitten ist - Kein "hm - Soll ich die Methode jetzt noch in dieses Interface packen oder nicht...?" mehr. Es ist viel einfacher, Mocks zur verwenden, da diese
    im besten Fall viel weniger implementieren müssen.

    Bsp. lazy evaluation: Das machen andere Sprachen ebenso - s. Javascript. Ich glaube nicht, dass das ein JS-Entwickler als unintuitiv ansieht. Alles eine Frage der Perspektive :-)

    Was in meinen Augen fehlt, sind dagegen partial functions und currying.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.