Funktionale Programmierung Th. Letschert TH Mittelhessen Gießen University of Applied Sciences Typen – Strukturelle und Nominelle Typisierung / Compound Types – Polymorphismus – Typklassen – Arten von Typen / Typen höherer Art – (Pfad-)abhängige Typen Typen Typen in Scala Ein Typ ist eine statische Information, d.h. eine Information die der Compiler aus dem Programmcode gewinnt. Scala ist statisch typisiert Der Compiler berechnet und prüft (Typ-) Informationen die Ausdrücken (Abschnitte des Quellcodes) zugeordnet sind Das Typsystem von Scala basiert auf dem von Java Die prinzipielle Vorgehensweise bei der Typprüfung und die Art der berechneten Typen entspricht der von Java. Es gibt aber Erweiterungen. Ziel der Typisierung Vermeidung von Laufzeitfehlern Erzeugung von effizientem Code Problem der Typisierung Statische Typisierung schränkt die Flexibilität der Entwickler ein: Es wird immer Programme geben, die vom Compiler / Typsystem zurück gewiesen werden, aber problemlos (mit manchen Eingabedaten) ablaufen könnten. Ein flexibles Typsystem minimiert die Zahl der zu Unrecht zurück gewiesenen Programme Je flexibler ein Typsystem ist, um so komplexer ist es. statically typed http://fsharpforfunandprofit.com/posts/ten-reasons-not-to-use-a-functional-programming-language/ Seite 2 Typen Typen in Scala Ein Typ kann auf zwei Arten definiert werden: – Indirekt Durch eine class, trait oder object-Definition Die Definition erzeugt eine assoziierten Typ mit dem gleichen Namen – Direkt Durch eine type-Definition Mit dem type-Schlüsselwort können direkt Typen definiert werden Seite 3 Typen Strukturelle oder Nominelle Typisierung Strukturell – Auch „Duck-Typing“ genennt: If it looks like a duck and behaves like a duck, it is a duck. – Nur die Struktur eines Typs ist bei der Typisierung relevant, die Namen / die Definitionen der Typen spielen keine Rolle Nominell – Nicht nur die Struktur, sondern auch – die Namen / Definitionen der Typen spielen eine Rolle bei der Typisierung trait def } trait def } Foo { m: Unit Bar { m: Unit def call(f: Foo): Unit = { f.m } call( new Foo {def m(): Unit = { println("Hello")}} ) call( new Bar {def m(): Unit = { println("Hello")}} ) Seite 4 Kein „Duck-Typing“, Nominelle Typisierung in Scala: Typfehler ! Typen Strukturelle oder Nominelle Typisierung Compound Types in Scala* – Typen die aus anderen Typen „zusammengesetzt“ sind Beispiel: class Meat trait Dog { def eat(m: Meat): Unit = {} } trait Evil { def bite(a: Any): Unit = {} } Compound Type def callEvilDog(bd: Dog with Evil): Unit = { bd.eat(new Meat); bd.bite(new Object) } object Pluto extends Dog with Evil callEvilDog(Pluto) * https://www.scala-lang.org/files/archive/spec/2.12/03-types.html#compound-types Seite 5 Typen Strukturelle oder Nominelle Typisierung Compound Types in Scala Beispiel 2, Compound Type mit Extension: class Meat trait Dog { def eat(m: Meat): Unit = println("eat") } Compound Type hier mit Extension trait Evil { def bite(a: Any): Unit = println("bite") } def callEvilDog(bd: Dog with Evil): Unit = { bd.eat(new Meat); bd.bite(new Object) } def callSingingEvilDog(bd: Dog with Evil { def sing: Unit }): Unit = { bd.eat(new Meat) bd.bite(new Object) bd.sing } class Pluto extends Dog with Evil callSingingEvilDog(new Pluto {def sing: Unit = println("Wau")} ) object Harras extends Dog with Evil { def sing: Unit = println("Wauwau") } callSingingEvilDog(Harras) Seite 6 Achtung: Extensions werden mit Reflection realisiert, also langsam! Polymorphismus Polymorphismus Ad-hoc Überladung def g(x: Int): Int = 2 * x def g(x: Double): Double = 2.0 + x g(2) g(2.0) def f(x: Double): Double = 2 * x Anpassung f(2) f(2.0) Universell Parametrisch def h[A](l: List[A]): A = l.head h(List(1,2,3)) h(List(1.0,2.0,3.0)) trait Animal { def sound: String } class Cow extends Animal { override def sound = "Muh" } val berta: Animal = new Cow berta.sound Subtyp Seite 7 Polymorphismus Polymorphismus Poly, πολύς – Viel Morph, μορφή – Gestalt Polymorphismus in Programmiersprachen Polymorph: Ein Wert / eine Variable hat mehr als einen Typ. (In der funktionalen Programmierung sind Funktionen ganz normale Werte) Eine Funktion akzeptiert Argumente / produziert Ergebnisse von unterschiedlichem Typ Varianten des Polymorphismus* Polymorphismus kann und wird in vielfältiger Art analysiert und klassifiziert. Allgemein üblich ist folgende Klassifizierung: – Universeller Polymorphismus Parametrischer Polymorphismus, Parametric Polymorphism Subtyp-Polymorphismus, Subtype Polymorphism, Inclusion Polymorphism – Ad-hoc Polymorphismus Anpassungs-Polymorphismus / Coercion Polymorphism Überladung / Overloading * von J. Reynolds in : Theories of Programming Languages, Cambridge University Press 1998, 2009 allgemein eingeführte Klassifikation Seite 8 Polymorphismus Parametrischer Polymorphismus def h[A](l: List[A]): A = l.head Seite 9 Parametrischer Polymorphismus Parametrischer Polymorphismus Ein (Funktions-) Wert ist parametrisch-polymorph, wenn er alle Typen von einer bestimmten Struktur hat Ebenfalls verbreitete Bezeichnung: Generischer Typ, Generics, … Beruht auf der Generizität vieler Operationen: – Gleiches in gleicher Art mit unterschiedlichen Dingen tun, – Z.B. eine Element mit einer bestimmten Eigenschaft in einer Liste suchen Beispiele: def find[T](lst: List[T], pred: T => Boolean) : Option[T] = lst.find { x => pred(x) } def double[T](f: T => T) : T => T = (x: T) => f(f(x)) def quadruple[T](f: T => T) : T => T = double(f) def fold[T, U](f:(T, U) => U, s: U) : List[T] => U = lst => lst match { case Nil => s case h :: t => f(h, fold(f, s)(t)) } Seite 10 Parametrischer Polymorphismus Parametrischer Polymorphismus Interpretation „Polymorphe Funktion“: Funktion mit Parameter vom Typ „Typ“ Beispiel: def find[T](lst: List[T], pred: T => Boolean) : Option[T] = lst.find { x => pred(x) } Find ist eine Funktion mit einen Typ-Parameter T Ein Typ-Argument, z.B. Int, das für den Typ-Parameter T übergeben wird, – wird als Argument für die Typ-Konstruktoren List und Option verwendet – damit werden die Typen List[Int] und Option[Int] konstruiert – und dann schließlich die Funktion def find(lst: List[Int], pred: Int => Boolean) : Option[Int] = lst.find { x => pred(x) } Seite 11 Parametrischer Polymorphismus Parametrischer Polymorphismus und Typausdrücke Type Erasure Bei parametrischem Polymorphismus ist jeder Typ als Typ-Argument erlaubt Eine generische Funktion kann darum nicht von irgendwelchen Eigenschaft / Besonderheiten eines bestimmten Typ-Arguments abhängen Der generische Typ ist irrelevant und kann entfernt werden. (Type Erasure) Typ-Ausdrücke mit Variablen = Typfunktionen Bei parametrischen Polymorphismus werden – mit Typ-Konstruktoren gebildete Typ-Ausdrücke – die freie Typ-Variablen (hier T, U) enthalten – als Funktionen über Typen gedeutet Beispiel: def fold[T, U]( f : (T, U) => U, s: U) : List[T] => U = … ~ fold : T:Type, U: Type => (T,U) => U => U => List[T] => U = … Typ-Argumente Typ Seite 12 Typkonstruktoren: List, => Typ-Variablen: T, U Parametrischer Polymorphismus Parametrischer Polymorphismus: Rang Rang-1 Polymorphismus auch: Rank-1 / Prenex Polymorphism, Prädikativer Polymorphismus Typ-Variablen können/dürfen hier nur durch nicht-generische Typen ersetzt werden. Beispiel: def equalAfterF[A, U](f: A => U, x: A, y: A) : Boolean = f(x) == f(y) println( equalAfterF( (s:String) => Integer.parseInt(s), "1", "01") ) Seite 13 Rang 1 polymorphe Funktion Rang-1: Typ-Parameter A und U werden durch die konkreten Typen String und Int ersetzt Parametrischer Polymorphismus Parametrischer Polymorphismus: Rang Rang-1 Polymorphismus equalAfterF( (l:List[Int]) => l.length, List[Int](1,2), List[Int](3,4)) equalAfterF( (l: List[Any]) => l.length, List(1), List("01")) def equalAfterF[A, U]( f: A => U, x: A, y: A) : Boolean = OK liefert true OK liefert true die beiden Listen werden auf List[Any] gecastet == equalAfterF[List[Any], Int]( (l: List[Any]) => l.length, List(1), List("01")) Seite 14 f(x) == f(y) Parametrischer Polymorphismus Parametrischer Polymorphismus: Rang def equalAfterF[A, U]( f: A => U, x: A, y: A) : Boolean = f(x) == f(y) Rang-1 Polymorphismus: – Polymorphe Funktionen sind keine „erst-klassigen“ Werte, die können beispielsweise nicht als Funktionsargumente auftreten. – Polymorphe Funktionen können also nicht (polymorph !) übergeben werden. Wird eine polymorphe Funktion als Argument übergeben, dann werden ihre Typ-Argumente bei der Übergabe festgelegt, und nicht bei Aufrufen innerhalb der Funktion, an die sie übergeben wurde. equalAfterF( (l:List[A]) => l.length, List[Int](1,2), List[String]("3","4")) def f[A](l:List[A]): Int =l.length equalAfterF( f, List[Int](1,2), List[String]("3","4")) nicht compilierbar, ungebundener Typ-Parameter A, kein Scala Ok, aber f wird bei der Übergabe zu einer nicht polymorphen Funktion instaniiert == equalAfterF[List[Any], Int]( (f _ ).asInstanceOf[Function[List[Any], Int]], List[Int](1,2), List[String]("3","4")) Seite 15 Parametrischer Polymorphismus Parametrischer Polymorphismus: Rang Rang-2 Polymorphismus – Polymorphe Funktionen sind „erst-klassige“ Werte, sie können beispielsweise als Funktionsargumente auftreten. – Polymorphe Funktionen können wirklich polymorph übergeben werden. Wird eine polymorphe Funktion als Argument übergeben, dann werden ihre Typ-Argumente nicht bei der Übergabe festgelegt, sondern bei jedem Aufruf innerhalb der Funktion, an die sie übergeben wurde. def equalAfterF[A, B, U]( f[T]: T => U, x: A, y: B) : Boolean = f(x) == f(y) Rank-2 polymorphe Funktion. Pseudocode, kein Scala! Wenn Scala Rang-2 Polymorphismus unterstützen würde, dann wäre eine solche Funktions-Definition und -Anwendung möglich. def strngify[T](x: T) : String = { x.toString } equalAfterF( strngify, 1, 1.0) Seite 16 Parametrischer Polymorphismus Parametrischer Polymorphismus: Rang Rang-2 Polymorphismus def equalAfterF[A, B, U]( f[T]: T => U, x: A, y: B) : Boolean = f(x) == f(y) def equalAfterF[T, A, B, U]( f: T => U, x: A, y: B) : Boolean = f(x) == f(y) ≠ Geht nicht! Geht, ist aber falsch! Rank-2 polymorphe Funktion. Pseudocode, kein Scala! Rank-1 polymorphe Funktion. Kein korrektes Scala! def strngify[T](x: T) : String = { x.toString } Error: type mismatch; found : x.type (with underlying type A) required: T y: B) : Boolean = f(x) == f(y) Error: type mismatch; found : y.type (with underlying type B) required: T y: B) : Boolean = f(x) == f(y) equalAfterF( strngify, 1, 1.0) Seite 17 Parametrischer Polymorphismus Parametrischer Polymorphismus: Rang Rang-2-Polymorphimus: Emulation in Scala mit Struktureller Typisierung / Compound Types Beispiel 1: def equalAfterF[A, B, U]( f: {def apply[T](x:T): U}, x: A, y: B) : Boolean = f(x) == f(y) „Struktureller Typ“ realisiert als Compound Type, der nur aus einer Extension besteht. def main(args: Array[String]): Unit = { val stringify = new { def apply[T](x:T): String = x.toString} equalAfterF( stringify, 1, 1.0) ~> false Seite 18 Typ: ∀T.(x: T): String Parametrischer Polymorphismus Parametrischer Polymorphismus: Rang Rang-2-Polymorphimus: Zusammenfassung Rang-2 Polymorphismus – erlaubt die Übergabe generischer Funktionen an Funktionen (ohne dass sie ihre Generizität bei der Übergabe verlieren) – macht also generische Funktionen zu „Werten erster Klasse“ – bedeutet, dass Werte mit quantifizierten Typen möglich sind z.B.: ∀T.List[T] => Int – kann in Scala emuliert werden durch Werte mit einem strukturellen Typ da in einer Klassendefinition – strukturelle Typen sind Klassendefinitionen – über Typparameter abstrahiert / quantifiziert werden kann z.B.: ∀T.List[T] => Int ~ { } def apply[T](x: List[T]): Int Seite 19 Polymorphismus Subtyp-Polymorphismus trait Animal { def sound: String } class Cow extends Animal { override def sound = "Muh" } Seite 20 Subtyp-Polymorphismus Subtyp-Polymorphismus Ein Wert ist Sub-Typ-polymorph wenn er alle Typen in einer bestimmten Hierarchie von Typen hat Ebenfalls verbreitete Bezeichnung: OO-Polymorphismus, … Beruht auf der Subtyp-Relation: – Ein Wert von einem Typ T hat auch alle Typen T', die Supertyp von T sind, von denen T ein Subtyp ist – In OO-Sprachen beruht die Subtyp-Relation auf der Vererbungs-Relation Beispiel: abstract class Animal class Dog extends Animal val a: Animal = new Dog Die Instanz der Klasse Dog hat den Typ Dog und den Typ Animal Seite 21 Subtyp-Polymorphismus Subtyp-Relation Beruht auf einer Hierarchie die basiert auf – Informationsgehalt und – Anwendbarkeit T ist ein Subtyp von T' : T < T' gdw. – jedes Objekt von Typ T mindestens das weiß und kann was ein Objekt on Typ T' weiß und kann Substitutionsprinzip: T < T' gdw. – jedes Objekt vom Typ T jedes Objekt vom Typ T' jederzeit ersetzen kann Seite 22 Subtyp-Polymorphismus Varianz Varianz: Wie variiert (überträgt sich) die Subtyp-Relation bei der Bildung neuer Typen mit Typ-Konstruktoren Beispiel: Dog < Animal class Animal class Dog extends Animal folgt daraus ? val animal: Animal = new Animal val dog: Dog = new Dog def f(animal: Animal) : Unit = {} f(animal) f(dog) Dog < Animal, also dog ist überall da erlaubt, wo ein Animal gefordert wird. List[Dog] < List[Animal] def fl(animals: List[Animal]) : Unit = {} fl(List(animal)) fl(List(dog)) Dog < Animal => List[Dog] < List[Animal] und darum dieser Aufruf erlaubt Seite 23 Subtyp- und parametrischer Polymorphismus Varianz Invarianz Die Subtyp-Eigenschaft überträgt sich nicht Co-Varianz Die Subtyp-Eigenschaft überträgt sich direkt Contra-Varianz Die Subtyp-Eigenschaft überträgt sich in ihr Gegenteil Beispiel: Varianz von Funktionstypen Der Typ-Konstruktor T1 => T2 ist contra-variant in seinem ersten Argument und co-variant in seinem zweiten Argument Dog < Animal ~> Animal => T < T => Dog < Dog => T T => Animal für jeden Typ T und für jeden Typ T Intuition (Substitutionsprinzip): – Contra-variant im Argument: Eine Funktion die ein Tier annimmt, kann verwendet werden, wenn ein Funktion gebraucht wird, die einen Hund akzeptiert. – Co-variant im Ergebnis: Eine Funktion, die einen Hund produziert kann an jeder Stelle verwendet werden, wo eine Tier-produzierende Funktion gebraucht wird. Seite 24 Subtyp- und parametrischer Polymorphismus Varianz Scala: Varianz von generischen Klassen – Generische Klassen sind Typ-Konstruktoren – Die Varianz des Typ-Konstuktors, der mit der generischen Klasse assoziiert ist, wird bei deren Definition festgelegt. – Beispiel: class Animal { def feed : Unit = {} } class Dog extends Animal class Container[T](val content: T) def feedIt(cage: Container[Animal]) : Unit = { cage.content.feed } val fifi = new Dog val basket = new Container[Dog](fifi) feedIt(basket) Fehler, keine Varianz: Ein Container[Dog] ist kein Subtyp von Container[Animal], darf also einen Container[Animal] nicht ersetzen. Seite 25 Subtyp- und parametrischer Polymorphismus Co-Varianz Scala: Co-Varianz definieren – Beispiel: class Animal { def feed : Unit = {} } class Dog extends Animal class Container[+T](val content: T) def feedIt(cage: Container[Animal]) : Unit = { cage.content.feed } val fifi = new Dog val basket = new Container[Dog](fifi) feedIt(basket) Kein Fehler, Container ist co-variant: Ein Container[Dog] ist ein Subtyp von Container[Animal], darf also einen Container[Animal] ersetzen. Seite 26 Subtyp- und parametrischer Polymorphismus Contra-Varianz Scala: Contra-Varianz definieren – Beispiel: class Animal { def feed : Unit = {} } class Dog extends Animal class FunContainer[-T] (val f: T=>Unit) def callIt(cage: FunContainer[Dog]) : Unit = { cage.f(new Dog) } val feeding = (a: Animal) => a.feed val basket : FunContainer[Animal] = new FunContainer[Animal] ((a: Animal) => a.feed) callIt(basket) Kein Fehler, FunContainer ist contra-variant: Ein FunContainer[Animal] ist ein Subtyp von FunContainer[Dog], darf also einen FunContainer[Dog] ersetzen. Contra-Varianz ist ungewöhnlich und entsprechende Beispiele stets etwas bemüht. Seite 27 Subtyp- und parametrischer Polymorphismus Varianz-Definition: Verwendungs- vs Deklarations-seitig Die Varianz kann definiert werden – bei der Deklaration eines Typs – oder bei der Verwendung eines Typs class Animal { def feed : Unit = {} } class Dog extends Animal class Container[+T](val content: T) Varianz-Definition bei der Typ-Definition def feedIt(cage: Container[Animal]) : Unit = { cage.content.feed } class Animal { def feed : Unit = {} } class Dog extends Animal class Container[T](val content: T) def feedIt(cage: Container[_ <: Animal]) : Unit = { cage.content.feed } Seite 28 Varianz-Definition bei der Verwendung des Typs Animal: feedIt kann genutzt werden wie oben. D.h. so als sei Container covariant. Subtyp- und parametrischer Polymorphismus Varianz-Definition: Verwendungs- vs Deklarations-seitig Java – alle Typkonstruktoren sind invariant, – die Definition einer Varianz eines Typs bei der Verwendung eines Typs ist möglich class Animal { public void feed() {} } class Dog extends Animal {} class Container<T> { T content; public Container(T content) { this.content = content; } public T getContent() { return content; } } void feedIt(Container<? extends Animal> cage) { cage.getContent().feed(); } Seite 29 Varianz-Definition bei der Verwendung des Typs Animal: feedIt kann genutzt so werden, als sei Container covariant. Subtyp- und parametrischer Polymorphismus Varianz – Zusammenfassung Scala In Scala wird die Varianz eines Typ-Konstruktors F – d.h. einer generischen Klasse – bei deren Definition festgelegt: – F[ T ] – F[+T] – F[-T ] F ist invariant F ist kovariant F ist kontravariant A = B ⇒ F[A] < F[B] A < B ⇒ F[A] < F[B] A < B ⇒ F[B] < F[A] T < T' bedeutet dabei dass T gleich T' oder ein Subtyp von T' ist: Objekte vom Typ T sind dann kompatibel mit solchen vom Typ T' Seite 30 Subtyp- und parametrischer Polymorphismus Varianz – Zusammenfassung Die Varianz eines generischen Typs kann beliebig definiert werden, man sollte sich aber an folgende Empfehlungen halten: – Unveränderliche Kollektionen sollten kovariant sein Eine Apfelkorb kann einen Fruchtkorb ersetzen, wenn man nichts hinein legen kann. Der Nutzer erwartet, dass der Korb Früchte enthält und wird nie enttäuscht werden. – Veränderliche Kollektionen sollten invariant sein Eine Apfelkorb kann nicht einen Fruchtkorb ersetzen, wenn man hinein legen und entnehmen darf: Der Nutzer des Apfelkorbs erwartet, dass der Korb Äpfel enthält und wird enttäuscht werden, wenn ein anderer ihn vorher als Fruchtkorb genutzt und eine Birne hineingelegt hat. – Funktionale Typen sollten kontravariant im Parametertyp und kovariant im Ergebnistyp sein Der Nutzer erwartet eine Presse die aus Früchten Fruchtsaft erzeugt, man enttäuscht ihn nicht, gibt man ihm eine Presse, die alle Pflanzenteile (contra-variant) zu Saft der Apfelsorte Golden-Delicious (co-variant) presst. Seite 31 Subtyp- und parametrischer Polymorphismus Typ-Grenzen Typ-Grenzen, Type Bounds Typ-Grenzen erlauben es die möglichen aktuellen Typ-Parameter durch eine Subtyp-Relation begrenzen. T <: T' : T' ist obere Grenze für T: T muss gleich T', oder spezieller als T' sein T ist dabei die ungebundene Typ-Variable Beispiel abstract class Fruit { def peel() = { } def press() : Double } class Apple extends Fruit { def press() : Double = 50.0 } class Cherry extends Fruit { def press() : Double = 3.0 } class Pear extends Fruit { def press() : Double = 75.0 } def makeJuice[T <: Fruit](v: List[T]) : Double = v.foldLeft(0.0)( _ + _.press() ) Nur Früchte können zu Fruchtsaft verarbeitet werden Seite 32 Abstrakte Typen Abstrakte Typen Eine Klasse / ein Trait kann einen abstrakten Typ definieren. Beispiel: abstract class BoxA { type T val content: T } object MyBoxA extends BoxA { override type T = Int override val content = 42 } T ist abstraktes Typ-Feld Seite 33 Abstrakte Typen Abstrakte Typen Haben sehr starke Ähnlichkeit mit Typ-Parametern Beispiel: abstract class BoxA { type T val content: T } abstract class BoxG[T] { val content: T } object MyBoxA extends BoxA { override type T = Int override val content = 42 } object MyBoxG extends BoxG[Int] { override val content = 42 } beide Varianten sind im Wesentlichen äquivalent T ist abstraktes Typ-Feld T ist generischer Typ-Parameter Seite 34 Abstrakte Typen Abstrakte Typen Abstrakte Typen können gebunden sein: Beispiel: abstract class Feed class Gras extends Feed class Meat extends Feed abstract class Animal { type MyFeed <: Feed def eat(f: MyFeed): Unit } Seite 35 Parametrischer Polymorphismus vs abstrakte Typen Abstrakte Typen vs Generische Typen vs Subtyp-Polymorphismus abstract class Feed class Gras extends Feed class Meat extends Feed abstract class Feed class Gras extends Feed class Meat extends Feed abstract class Animal { type MyFeed <: Feed def eat(f: MyFeed): Unit } abstract class Animal { def eat(f: Feed): Unit = println("eating ??") } case object Cow extends Animal { type MyFeed = Gras override def eat(f: Gras) : Unit = println("eating gras") } case object Cow extends Animal { def eat(f: Gras) : Unit = println("eating gras") } case object Dog extends Animal { type MyFeed = Meat override def eat(f: Meat) : Unit = println("eating meat") } case object Dog extends Animal { def eat(f: Meat) : Unit = println("eating meat") } Cow.eat(new Gras) Dog.eat(new Meat) Cow.eat(new Gras) Dog.eat(new Meat) Cow.eat(new Meat) Dog.eat(new Grass) Cow.eat(new Meat) Dog.eat(new Grass) type mismatch Das ist so wie es sein soll. Fleisch-fressende Kühe, igitt Seite 36 Ad-hoc Polymorphismus Überladung def g(x: Int): Int = 2 * x def g(x: Double): Double = 2.0 + x Seite 37 Ad-hoc Polymorphismus Überladung Eine Funktion / Methode gibt es im mehreren Varianten diese unterscheiden sich im Typ der Argumente und in ihrer Implementierung Beispiel: // MyVector mit überladener *-Methode case class MyVector(x: Double, y: Double) { def *(x: Double) : MyVector = MyVector(x*this.x, x*this.y) def *:(x: Double) : MyVector = MyVector(x*this.x, x*this.y) def *(other: MyVector) : Double = this.x*other.x + this.y*other.y } // überladene mult-Funktion def mult(x: Double, v: MyVector) : MyVector = MyVector(x*v.x, x*v.y) def mult(v1: MyVector, v2: MyVector) : Double = v1.x*v2.x + v1.y*v2.y val v1 = MyVector(1,2) val v2 = MyVector(2,3) println( v1 * 2 ) println( 2 *: v1) println( mult(2, v1) ) println( mult(v1, v2) ) Seite 38 Ad-hoc Polymorhismus Überladung mit Value Classes Value Class – Eine Klasse mit beschränkten Möglichkeiten – Ermöglicht es Methoden an einen Wert zu binden (z.B. Konverionsoperationen) ohne LaufzeitOverhead – Entspricht und wird compiliert in statische Methodenaufrufe Beispiel – Teil 1 case class Complex(re: Double, im: Double) { def *(x: Double) : Complex = Complex(re*this.re, im*this.im) def *(other: Complex) : Double = this.re*other.re + this.im*other.im } object Complex { case class WrappedDouble(x: Double) { def *(v : Complex) : Complex = v * x } implicit def convert(x: Double): WrappedDouble = WrappedDouble(x) } object ValueClass_App extends App { val v1 = Complex(1,2) val v2 = Complex(2,3) println( v1 * v2 ) println( v1 * 2 ) println( 2 * v1 ) } Implicit conversions found: 2 => convert(2) Seite 39 Die notwendige Hüllenklasse (WrappedDouble) verursacht Speicher- und Laufzeit-Aufwand Ad-hoc Polymorhismus Überladung mit Value Classes Value Class – Eine Klasse mit beschränkten Möglichkeiten – Ermöglicht es Methoden an einen Wert zu binden (z.B. Konverionsoperationen) ohne LaufzeitOverhead – Entspricht und wird compiliert in statische Methodenaufrufe Beispiel – Teil 2 case class Complex(re: Double, im: Double) { def *(x: Double) : Complex = Complex(re*this.re, im*this.im) def *(other: Complex) : Double = this.re*other.re + this.im*other.im } object Complex { case class WrappedDouble(x: Double) extends AnyVal { case class WrappedDouble(x: Double) { def *(v : Complex) : Complex = v * x def *(v : Complex) : Complex = v * x } } implicit def convert(x: Double): WrappedDouble = WrappedDouble(x) } object ValueClass_App extends App { val v1 = Complex(1,2) val v2 = Complex(2,3) Zur Laufzeit werden keine Instanzen der Klasse WrappedDouble erzeugt. Der Compiler erzeugt statt dessen eine statische *–Methode mit der Signatur * (x: Double, v: Complex) println( v1 * v2 ) println( v1 * 2 ) println( 2 * v1 ) } Seite 40 Ad-hoc Polymorphismus Anpassungs-Polymorphismus (Coersions) def g(x: Double): Double = 2.0 + x g(2) Seite 41 Anpassungs-Polymorhismus Anpassungs-Polymorphismus (Coercion Polymorphism) Ein Wert (eine Funktion) ist anpassungs-polymorph, wenn er – mit Anpassungs-Operationen in andere Werte transformiert werden kann – und diese Typen nicht in einer Subtyp-Relation stehen, oder die gleiche Struktur haben Beispiel – +-Operator: ist auf allen numerischen Typen und Strings definiert. + : Int, Int => Int ... + : String, String => String – Dabei wird die + : String, String => String oft die Anpassungsoperation _.toString ermöglicht. – Überladungen und Anpassungsoperationen (Coercions) kooperieren wie in diesem Beispiel oft – Die Anpassungsoperationen (hier z.B. _.toString) sind keine Up- oder Down-Casts, haben also keinen Bezug zu Subtyp-Relationen! Seite 42 Anpassungs-Polymorhismus Anpassungs-Polymorphismus (Coercion Polymorphism) Scala: implizite Anpassungs-Operationen – Anpassungs-Polymorphismus kann in Scala mit impliziten Anpassungs-Operationen realisiert werden – Beispiel: case class Complex(re: Double, im: Double) { def +(other: Complex) : Complex = Complex(this.re+other.re, this.im+other.im) } implicit def toComplex(d: Double): Complex = Complex(d, 0.0) println( Complex(1.0, 2.0) + 12.0 ) Complex(13.0,2.0) Seite 43 Anpassungs-Polymorhismus Implizite Definitionen in Scala Implizite Definitionen erlauben es dem Programmierer Dinge wegzulassen, die der Compiler dann zur Übersetzungszeit ergänzt. Implizite Definitionen sind ein wichtiges und häufig eingesetztes Ausdrucksmittel in Scala. – Auch, aber nicht nur zur Definition von Anpassungs-Ploymorphismus Der Compiler kann im Prozess der – implicit resolution / Auflösung impliziter Referenzen folgende implizite Definitionen ergänzen – Fehlende Parameter in einem Methoden- / Konstruktor-Aufruf – Fehlende Konversionsoperationen in einem Methoden-Aufruf Seite 44 Anpassungs-Polymorhismus Implizite Definitionen in Scala Das Schlüsselwort implicit markiert zwei unterschiedliche Dinge: – Implizite Definitionen, Quelle: Werte die implizit verwendet werden können – Implizite Verwendungen, Senken: Stellen an denen ein implizite Werte eingesetzt werden können. Beispiel: Quelle eines impliziten Werts implicit val x = 42 def f(implicit arg: Int): String = "f uses arg " + arg implicit resolution println( f ) Senke eines impliziten Werts f uses arg 42 println(f(?)) Compiler (implicit resolution) Seite 45 println(f(x)) Polymorhismus Beispiel: Subtyp- vs Anpassungs-Polymorphismus Beispiel: Nur unter geordneten Dingen kann man das Minimum finden Realisation mit Subtyp-Polymorphismus: case class Complex(re: Double, im: Double) extends Ordered[Complex] { override def compare(other: Complex): Int = (Math.sqrt(this.re*this.re + this.im*this.im) Math.sqrt(other.re*other.re + other.im*other.im)).toInt } def min[T <: Ordered[T]](lst: List[T]): T = lst.reduce( (acc, v) => if (v <= acc ) v else acc ) val lst = List(Complex(1, 2), Complex(2, 3), Complex(3, 4)) val v = min(lst) println(v) Problem: Bei einer Typdefinition müssen zukünftige Verwendungsmöglichkeiten vorher gesehen werden. Seite 46 Polymorhismus Beispiel: Subtyp- vs Anpassungs-Polymorphismus Beispiel: Nur unter geordneten Dingen kann man das Minimum finden Realisation mit Anpassungs-Polymorphismus: case class Complex(re: Double, im: Double) def min[T](lst: List[T])(implicit asOrdered: T => Ordered[T]): T = lst.reduce( (acc, v) => if (v <= acc ) v else acc ) unabhängig von einander definieren implicit def toOrdered(c: Complex) : Ordered[Complex] = new Ordered[Complex] { def compare(other: Complex): Int = (Math.sqrt(c.re*c.re + c.im*c.im) Math.sqrt(other.re*other.re + other.im*other.im)).toInt } zusammen führen val lst = List(Complex(1, 2), Complex(2, 3), Complex(3, 4)) val v = min(lst) println(v) Problem: Relativ komplex. Verwendung von implicit nur mit Vorsicht. Seite 47 Typklassen Seite 48 Typklassen Typklassen Typklassen haben nichts mit OO-Klassen zu tun! Typklasse / Type Class – Eine Typklasse ist eine Klasse von Typen, d.h. eine Art von Typ, oder ein Typen-Typ – Einsatz: Typen mit bestimmten Eigenschaften definieren Beispiel: Typen mit der Eigenschaft geordnet zu sein. – Verbreitet in statisch typisierten rein funktionalen Sprachen (z.B. Haskell) da diese keine Klassen und darum keine Subtyp-Relationen auf Basis der Vererbung kennen – Subtypen können genutzt werden um Typklassen zu definieren: Die Klasse der Typen die Subtyp von T sind – Typklassen erlauben aber eine flexiblere „ad-hoc“ Definition von Arten von Typen – Typklassen sind eine Form des ad-hoc Polymorphismus Seite 49 Typklassen Typklassen in Haskell Haskell bietet Typklassen – Arten von Typen – als Sprachkonstrukt (im Gegensatz zu Scala) Beispiel Eq die Klasse der Typen mit Gleichheit und die Klasse Ord der Typen mit Vergleichsoperationen (in Haskell werden Typen mit Kleinbuchstaben bezeichnet) class Eq a where (==) :: a → a → Bool (!=) :: a → a → Bool Typ a gehört zur Klasse Eq wenn er folgende Funktionen bietet: die Funktion == mit dem Typ a → a → Bool ... („::“ bedeutet „hat den Typ“) class Eq a => Ord a where (<) :: a → a → Bool (>) :: a → a → Bool ... Die Typklasse Ord ist einer Erweiterung der Typklasse Eq. Typ a gehört zur Klasse Ord wenn er zu den Funktionen von Eq noch folgende Funktionen bietet: die Funktion < mit dem Typ a → a → Bool ... Typklassen werden über ihre Fähigkeiten definiert. Sie sind darum mit Interfaces vergleichbar. Seite 50 Typklassen Typklassen in Haskell Typen können als Mitglieder einer Typklasse deklariert werden Beispiel Nat die Klasse der natürlichen Zahlen als Typ mit Gleichheit instance Eq Nat where x == y = natEq x y Typ Nat gehört zur Klasse Eq wobei die abstrakte Funktion == als natEq zu interpretieren ist. Typbeschränkungen via Typklassen Typklassen werden zur Beschränkung von generischen Parametern verwendet Beispiel: Sortieren kann man nur Dinge die geordnet sind sort :: (Ord a) => [a] → [a] Die Funktion sort ist generisch im Typ a. Der Typ a muss zur Typklasse Ord gehören. sort hat den Typ [a] → [a] ( [a] ~ Liste von a-Werten) Typklassen sind ein sehr einfacher gut verständlicher Mechanismus zur Beschreibung / Einschränkung von TypParametern. Seite 51 Typklassen Typklassen in Scala Typklassen sind kein Sprachmittel von Scala Typklassen können aber mit dem „syntaktischer Zucker“ impliziter Definitionen emuliert werden. Beispiel / Variante mit implizitem Objekt als Parameter: case class Complex(re: Double, im: Double) def min[T](lst: List[T])(implicit o: MkOrdered[T]): T = lst.reduce( (acc, v) => { if (o(v) <= acc) v else acc }) Das Minimum aller Elemente einer Liste kann nur berechnet werden, wenn die Elemente vergleichbar sind. Hier: wenn sie mit einem Objekt o in Vergleichbares konvertiert werden können. trait MkOrdered[T] { def apply(x: T) : Ordered[T] } val lst = List(Complex(1, 2), Complex(2, 3), Complex(3, 4)) implicit object MkOrderedComplex extends MkOrdered[Complex] { MkOrderedComplex ist so ein o, es kann def apply(c: Complex) : Ordered[Complex] = new Ordered[Complex] { def compare(other: Complex): Int = Complex-Objekte vergleichbar machen. (Math.sqrt(c.re*c.re + c.im*c.im) Math.sqrt(other.re*other.re + other.im*other.im)).toInt } } Minimum berechnen. val v = min(lst) println(v) Seite 52 Typklassen Typklassen in Scala Syntaktischer Zucker: Mit Implizite Konversionen sieht es so aus, als gäbe es Typklassen in Scala: Typklassen werden emuliert. lies: „T gehört zu der Typklasse MkOrdered.“ case class Complex(re: Double, im: Double) def min[T](lst: List[T])(implicit o: MkOrdered[T]): T = äquivalente lst.reduce( (acc, v) => { Definitionen if (o(v) <= acc) v else acc }) def min[T: MkOrdered](lst: List[T]): T = lst.reduce( (acc, v) => { trait MkOrdered[T] { if (implicitly[MkOrdered[T]](v) <= acc) def apply(x: T) : Ordered[T] }) } v else acc val lst = List(Complex(1, 2), Complex(2, 3), Complex(3, 4)) implicit object MkOrderedComplex extends MkOrdered[Complex] { def apply(c: Complex) : Ordered[Complex] = new Ordered[Complex] { def compare(other: Complex): Int = (Math.sqrt(c.re*c.re + c.im*c.im) Math.sqrt(other.re*other.re + other.im*other.im)).toInt } } val v = min(lst) println(v) Seite 53 Syntaktisch gezuckerte Version. Der syntaktische Zucker macht sie für funktionale Programmierer konsumierbar. Typen höherer Art Seite 54 Typen höherer Art / Ordnung Typen höherer Art / Ordnung – Higher Kinded Types Polymorphismus höherer Ordnung / Typ-Konstruktor-Polymorphismus handelt von Typen höherer Art / Higher Kinded Types Typen höherer Art sind generische Typen, deren Parameter kein Typ, sondern ein Typ-Konstruktor ist Typ-Konstruktoren Ein Typ-Konstruktor ist etwas das aus einem oder mehreren Typen einen Typ konstruieren kann Beispiele – List: Int => List[Int] – Map: Int, String => Map[Int, String] Typen höherer Art abstrahieren also über Typ-Konstruktoren d.h. es sind Funktionen mit Typ-Konstruktoren als Parameter List => … Map => … Seite 55 Typen höherer Art / Ordnung Typen höherer Art / Ordnung / Higher Kinded Types – Das Problem Typ-Konstuktoren als Parameter einer Typkonstruktion Das Problem / Notwenigkeit einer neuen Abstraktionsstufe trait Mapable[A] { def map[B](f: A => B): Mapable[B] } Mapable: etwas mit einer map-Methode, die wieder etwas mit map liefert. case class MyPair[T](a: T, b: T) extends Mapable[T] { override def map[B](f: T => B): Mapable[B] = MyPair(f(a), f(b)) } Paare mit map-Methode val p1: MyPair[String] = MyPair("Hallo", "Welt") val p2a: Mapable[Int] = p1.map( (x:String) => x.length) val p2b: MyPair[Int] = p1.map( (x:String) => x.length) Fehler: type mismatch; found : Mapable[Int] required: MyPair[Int] map liefert ein Mapable kein Paar – wie man erwarten könnte Wir wollen eigentlich sagen: map liefert ein Mapable von der gleichen Art! Aber wie? Seite 56 Typen höherer Art / Ordnung Typen höherer Art / Ordnung – Das Problem Map sollte einen Container vom gleichen Typ zurück liefern. Mit einer kovarianten Modifikation des Ergebnistyps lässt sich dies erreichen. trait Mapable[A] { def map[B](f: A => B): Mapable[B] } /* case class MyPair[T](a: T, b: T) extends Mapable[T] { override def map[B](f: T => B): Mapable[B] = MyPair(f(a), f(b)) } */ case class MyPair[T](a: T, b: T) extends Mapable[T] { override def map[B](f: T => B): MyPair[B] = MyPair(f(a), f(b)) } val p1: MyPair[String] = MyPair("Hallo", "Welt") val p2a: Mapable[Int] = p1.map( (x:String) => x.length) val p2b: MyPair[Int] = p1.map( (x:String) => x.length) Allerdings: Auf jeder Ableitungsstufe muss map in dieser Art redefiniert werden. Wenn die Hierarchie über mehrere Stufen mit abstrakten Klassen oder Interfaces geht, dann kann das lästig werden. Seite 57 Klasse von oben, wird ersetzt durch folgende Map liefert spezielleres Ergebnis. OK, da polymorphe Redefinition mit covariantem Ergebnis erlaubt sind. OK: Ergebnis MyPair[Int] Typen höherer Art / Ordnung Typen höherer Ordnung / Art – Typ-Konstruktoren als Parameter Der Typ-Konstruktor wird zum generischen Parameter der übergeben werden kann trait Mapable[A, Container[_]] { def map[B](f: A => B): Container[B] } Mapable ist „higher-kinded“: es hat keinen Typ, sondern einen Typkonstruktor Container als generischen Parameter case class MyPair[T](a: T, b: T) extends Mapable[T, MyPair] { override def map[B](f: T => B): MyPair[B] = MyPair(f(a), f(b)) } val p1: MyPair[String] val p2a: Mapable[Int, MyPair] val p2b: MyPair[Int] = MyPair("Hallo", "Welt") = p1.map( (x:String) => x.length) = p1.map( (x:String) => x.length) Seite 58 Typen höherer Art / Ordnung Die Arten eines Typs – Kinds of a Type Typen haben unterschiedliche Typen / Arten (Kinds)* – * (ausgesprochen „Typ“ ) Normale / echte Typen, die Ausdrücken / Werten zugeordnet sind, Beispiel: Int :: * List[Int] :: *, Int => Int :: *, Dies sind sind alles echte Typen von der Art (Kind) * – * ~> * Typ-Konstruktoren / Typ-Operatoren mit einem Argument, Beispiel: List :: * ~> * List macht aus einem Typ einen neuen Typ – * ~> * ~> * Typ-Konstruktoren / Typ-Operatoren mit zwei Argumenten (hier „gecurryt“) Beispiel: Pair :: * ~> * ~> * => :: * ~> * ~> * Pair und => machen aus zwei Typen einen neuen Typ echte Typen Typkonstruktoren Typkonstruktoren * Nach: Kapitel 29 von B.C. Pierce Types and Programming Languages. Gut zusammengefasst in Wikipedia, siehe https://en.wikipedia.org/wiki/Kind_(type_theory) Seite 59 Typen höherer Art / Ordnung Die Arten eines Typs – Kinds of a Type Typen haben unterschiedliche Typen / Arten (Kinds) – fortgesetzt – (* ~> *) ~> * Typ-Konstruktoren / Typ-Operatoren mit einem Argument der Art (* ~> *) also Typkonstruktoren mit einem Typkonstruktor als Argument – * ~> (* ~> *) ~> * Typ-Konstruktoren / Typ-Operatoren mit einem Argument der Art * und einem der Art (* ~> *) also Typkonstruktoren mit einem Typ und einem Typkonstruktor als Argument Beispiel: * ~> (* ~> *) ~> * * * ~> * trait Mapable[A, Container[_]] { def map[B](f: A => B): Container[B] } Seite 60 Typen höherer Ordnung / Art Typen höherer Ordnung / Art Typen höherer Art / Ordnung Die Arten eines Typs – Kinds of a Type Arten / Kinds in der REPL $ scala -feature Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_45). Type in expressions to have them evaluated. Type :help for more information. scala> import scala.language.higherKinds import scala.language.higherKinds scala> :kind -v Int scala.Int's kind is A * This is a proper type. +: covariant scala> :kind -v List scala.collection.immutable.List's kind is F[+A] * -(+)-> * This is a type constructor: a 1st-order-kinded type. scala> :kind -v trait Mapable[A, Container[_]] { def map[B](f: A => B): Container[B] } Mapable's kind is X[A1,F[A2]] * -> (* -> *) -> * This is a type constructor that takes type constructor(s): a higher-kinded type. scala> Seite 61 Abhängige Typen Seite 62 Typen und Pfade: Abhängige Typen Abhängige Typen Abhängige Typen: Typen, die von einem Wert abhängen Beispiel Programmiersprache Idris* Typ SizedClass: eine Klasse (Record-Typ) SizedClass mit zwei Felder: students: Ein Vektor fester Länge size von Studenten className: Ein String record SizedClass (size : Nat) where constructor SizedClassInfo students : Vect size Person className : String *Siehe http://docs.idris-lang.org/ Seite 63 Typen und Pfade Pfad-abhängige Typen In Scala (und Java) kann ein Typ von einem Objekt abhängig sein. Beispiel (Java und Scala) : Eine (Java: nicht-statische) innere Klasse hängt immer von dem Objekt ab, das sie definiert / erzeugt hat case class MyList(x: Int*) extends Iterable[Int] { def iterator = new Iterator[Int] { var pos = 0; def hasNext = pos < x.length def next: Int = { val v = x(pos) pos = pos+1 v } } case class MyList(x: Int*) extends Iterable[Int] { class MyIterator extends Iterator[Int] { var pos = 0; def hasNext = pos < x.length def next: Int = { val v = x(pos) pos = pos+1 v } } = def iterator = new this.MyIterator } } der hier definierte (anonyme) Typ hängt vom Wert x im Objekt this ab. der hier definierte Typ MyIterator hängt vom Wert x im Objekt this ab. Seite 64 Typen und Pfade Pfad-abhängige Typen In Scala werden abstrakte Typen als Pfad-abhängige Typen instanziiert Beispiel: abstract class Feed class Gras extends Feed class Meat extends Feed abstract class Animal { type MyFeed <: Feed def eat(f: MyFeed): Unit } case class Cow(name: String) extends Animal { type MyFeed = Gras override def eat(f: Gras) : Unit = println("eating gras") } val berta = new Cow("Berta") println((new berta.MyFeed).getClass.getSimpleName) Pfad-abhängiger Typ Seite 65 Gras Typen und Pfade Pfad-abhängige Typen Beispiel Emulation abhängiger Typen in Scala durch pfadabhängige Typen Arrays der Länge n als Pfad-abhängiger Typ: case class OfLength(n: Int) { class Arraj { private val _a: Array[Int] = new Array[Int](n) def apply(x: Int) = _a(x) def update(x: Int, y: Int): Unit = { _a(x) = y } } } val ofLength4 = OfLength(4) val ofLength5 = OfLength(5) Typen, die von einem Wert abhängen Arrays der Länge 4 und 5 type ArrayOfLength4 = ofLength4.Arraj type ArrayOfLength5 = ofLength5.Arraj def f(a: ArrayOfLength5): Unit = { a(4) = 4 } val a4 = new ArrayOfLength4 val a5 = new ArrayOfLength5 f(a4) f(a5) Typfehler ! Seite 66 Typen und Pfade Pfad-abhängige Typen Scala-Code ist typischerweise stark verschachtelt. Der Pfad eines Typs spielt darum eine wichtige Rolle. Pfade sind Referenzen die Objekte identifizieren Beispiel: package p3 trait T { def g(x: Int): Int } class C(v : Int) { class CT extends T { override def g(x: Int): Int = v+x } } object TypePathApp extends App { val c = new p3.C(4) // = new C(4) val ct = new c.CT println( ct.g(5) ) } p3, c und ct sind hier Pfade p3 und c sind die Pfade von Typen (p3.T p3.C), ct ist der Pfad einer Methode (ct.g) und eines Typs (c.CT) Seite 67 Typen und Pfade Pfade in der Scala-Spezifikation: http://www.scala-lang.org/files/archive/spec/2.12/03-types.html Seite 68 Typen und Pfade Pfad.this pfad.T.this bezieht sich auf das Objekt vom Typ pfad.T Der Pfad muss in der aktuellen Klassen-Verschachtelung liegen. Beispiel: trait T { val t = "T" } class C(v : Int) { val c = "C-"+v val t = "C-t" class CT extends T { val ct = "CT-"+v def g = { println( ct + " = " + this.ct + " = " + CT.this.ct ) println( C.this.c + " = " + c ) println( C.this.t ) println( t ) T is not an enclosing class //ERROR println( T.this.t ) } } } object TypePathApp extends App { val c = new C(4) val ct = new c.CT ct.g } Seite 69 CT-4 = CT-4 = CT-4 C-4 = C-4 C-t T Typen und Pfade Pfad-abhängige Typen / Path Dependent Types Innere Klassen, die zu unterschiedlichen Instanzen der äußeren Klasse gehören – die also unterschiedliche Pfade haben – werden strikt unterschieden Beispiel: Pfad-abhängiger Typ path-dependent type class Outer { trait Inner def y = new Inner {} def fpd(x: /*optional this.*/Inner) = x } Inner = this.Inner: der Typ Inner dieser Instanz von Outer object PathDependentTypeApp extends App { val a = new Outer val b = new Outer println( a.y.getClass() ) println( a.fpd(a.y).getClass() ) println( a.fpd(b.y).getClass() ) } type mismatch; found : p3.PathDependentTypeApp.b.Inner required: p3.PathDependentTypeApp.a.Inner dieses Inner-Objekt mag ich nicht! Seite 70 Typen und Pfade Typ-Projektion Pfad-abhängiger Typen Mit einem #T kann die Pfad-Abhängigkeit eines Typs aufgehoben werden Beispiel: class Outer { trait Inner def y = new Inner {} def fpd(x: /*optional this.*/Inner) = x def fproj(x: Outer#Inner) = x } Outer#Inner: der Typ Inner irgendeiner Instanz von Outer object PathDependentTypeApp extends App { val a = new Outer val b = new Outer println( a.fproj(a.y).getClass() ) // OK println( a.fproj(b.y).getClass()) // OK } Seite 71 Typen Typen in Scala – Das Typsystem von Scala ist komplex, auf den ersten Blick sogar verwirrend, Es braucht Zeit und Geduld bis die notwendige Neuverdahtung der Synapsen vollzogen ist – Der Grund ist die Vereinigung von objektorientierten und funktionalen Konzepten: Komplexität(OO+FP) = Komplexität(OO)Komplexität(FP) – Die Beschäftigung damit lohnt sich aber: OO + Funktional ist die Zukunft professioneller Programmiersprachen Das funktionale Paradigma versteht man besser im Vergleich / Kontrast / Kooperation mit dem objektorientierten Paradigma Die hier vorgestellten Typisierungskonzepte finden sich in allen modernen (typisierten) Programmiersprachen, wenn auch eventuell in Form von etwas anderen konkreten Sprachkonstrukten Die Ausdruckskraft von Scala erlaubt es die Typisierungs-Konzepte anderer Sprachen zumindest zu emulieren. Seite 72