Programmiersprachen – Konzepte und Realisationen Th. Letschert TH Mittelhessen Gießen University of Applied Sciences Programmierparadigmen I: Funktionale Programmierung – Programmierparadigmen – Das funktionale Paradigma Paradigmen der Programmierung Paradigma Ursprünglich: Beispiel das eine Art charakterisiert Allgemeiner Sprachgebrauch: Lehrmeinung, Gedankengebäude Karriere des Begriffs Startet mit Thomas Kuhn: The Structure of Scientific Revolutions (1962) deutsch: Die Struktur wissenschaftlicher Revolutionen, Suhrkamp Taschenbuch Wissenschaft, 13. Auflage 1996 These von Kuhn: Wissenschaftlicher Fortschritt ist nicht kontinuierlich Paradigma: Die akzeptierte Lehrmeinung bewegt sich meist in einem festen Rahmen, Paradigma genannt Von Zeit zu Zeit werden die Begrenzungen des herrschenden Paradigmas sichtbar Paradigmenwechsel: In eine Umbruch- und Kampf-Phase setzt sich dann ein neues Paradigma durch. Wissenschaftlicher Fortschritt kann damit (auch) als sozialer Prozess verstanden werden Seite 2 Paradigmen der Programmierung Paradigmen der Programmierung Ein Programmierparadigma ist ist eine Menge an kohärenten Konzepten und Prinzipien, die bei der Gestaltung einer software-technischen Lösung eingesetzt werden können. Paradigmen definieren Schemata zur Lösung von Problemen mit Hilfe von Computern: – Wie modelliert man Sachverhalte: Mit Listen, Abbildungen, Mengen, Relationen Mit Relationen Mit Klassen und Objekten Mit logischen Aussagen … – Wie formuliert man Berechnungen Mit Variablen, Zuweisungen, Bedingungen, Schleifen Mit Funktionen die Funktionen nutzen und selbst Funktionen erzeugen können Mit Regeln die logische Schlussfolgerungen erlauben Mit Regeln zur Manipulation von Termen ... Seite 3 Paradigmen der Programmierung Paradigmen der Programmierung Eine Programmiersprache unterstützt meist mehr als ein Programmierparadigma. Ein Programmierparadigma wird in der Regel von vielen Programmiersprachen unterstützt. Grundlegende (klassische) Programmierparadigmen – Imperativ Programme sind Anweisungen – eventuell zu (rekursiven) Prozeduren zusammengefasst. Sie manipulieren den Inhalt von Variablen. Die Sprachimplementierung führt die Anweisungen aus. – Objektorientiert Programme bestehen aus Objekten die gegenseitig Methoden aufrufen. Objekte sind Instanzen von Klassen in (polymorphen) Vererbungshierarchien. Die Sprachimplementierung erzeugt die Objekte und führt die Methodenaufrufe aus. – Funktional Programme bestehen aus Funktionsdefinitionen. Funktionen transformieren Werte. Funktionen selbst sind Werte. Die Sprachimplementierung berechnet die Werte von Ausdrücken mit Funktionen. – Logisch Programme sind logische Charakterisierungen von gesuchten Lösungen. Die Sprachimplementierung findet die Lösung an Hand ihrer Beschreibung. Seite 4 Paradigmen der Programmierung Paradigmen und Konzepte der Programmierung Programmiersprachen nutzen Konzepte die – wesentlich feiner aufgeteilt werden können, als es die grobe Taxonomie „imperativ, objektorientiert, funktional und logisch“ erlaubt, und die – in unterschiedlichsten Mischungen in einer Sprache auftreten können. Seite 5 Paradigmen der Programmierung Programmierparadigmen: Übersicht Quelle: http://www.info.ucl.ac.be/~pvr/paradigms.html Seite 6 Funktionale Programmierung Grundprinzipien der funktionalen Programmierung 1. Zeit und zeitliche Reihenfolge spielen keine Rolle bei der Programmdefinition Programme werden nicht als Aktionen in der Zeit definiert Die Ausführung läuft natürlich in der Zeit ab, aber dieser Ablauf wird nicht vom Programmierer (ungeschickt, eventuell fehlerhaft) sondern vom System (korrekt und angepasst an die aktuelle Situation zur Ausführungszeit) festgelegt 2. Die Definitionen von Funktionen ist das Strukturierungsmittel für Programme Rekursion: Werte inklusive Funktionen können mit Bezug auf andere Werte definiert werden Funktionen höherer Ordnung: Alles ist ein Wert insbesondere sind auch Funktionen Werte 3. Funktionsaufrufe sind die einzige „Kontrollstruktur“ Alle Berechnungen sind Berechnungen des Werts eines Ausdrucks. Der Ablauf einer Berechnung wird ausschließlich über die Organisation Funktionsaufrufen gesteuert. Seite 7 Funktionale Programmierung Konzepte der Funktionalen Programmierung In der funktionalen Programmierung wird eine Vielzahl von Konzepte mit unterschiedlicher Bedeutung verwendet: – Funktionen sind Werte. Es gibt Ausdrücke deren Wert eine Funktion ist (Closure) – Es gibt Ausdrücke deren Wert eine (mehr oder weniger) komplexe Datenstruktur ist – Induktive / algebraische Typen – Call-by-need / Call-by-name werden unterstützt – Funktionale (Map / Fold / Reduce) stehen zur Verfügung – Es gilt referenzielle Transparenz – Es gibt (scheinbar) unendlich große Datensequenzen – Parallelverarbeitung muss nicht explizit formuliert werden – ... Seite 8 Funktionale Programmierung: Was ist das und wozu gibt es das? Funktionale Programmierung ist eine Software-Engineering-Methode Programme sind in Funktionen organisiert – nicht in Pakete, Klassen, Objekte etc. Functional programming is so called because a program consists entirely of functions. The main program itself is written as a function which receives the program's input as its argument and delivers the program's output as its result. Typically the main function is defined in terms of other functions, which in turn are defined in terms of still more functions, until at the bottom level the functions are language primitives. aus: Why Functional Programming Matters von John Hughes http://www.cse.chalmers.se/~rjmh/Papers/whyfp.html Ein (der) Klassiker der funktionalen Programmierung! Seite 9 Funktionale Programmierung: Was ist das und wozu gibt es das? Funktionale Programmierung : Wenger ist Mehr Modulare Programmierung ist der Schlüssel zum Erfolg. Der Verzicht auf nicht-funktionale Features – wie Implizite Zustände und Seiteneffekte erlaubt / verbessert die modulare Programmierung. Modular design brings with it great productivity improvements. First of all, small modules can be coded quickly and easily. Secondly, general purpose modules can be re-used, leading to faster development of subsequent programs. Thirdly, the modules of a program can be tested independently, helping to reduce the time spent debugging. aus John Hughes: Why Functional Programming Matters Seite 10 Funktionale Programmierung: Was ist das und wozu gibt es das? Modulare Entwicklung : der „Kleber“ für Module ist entscheidend Modulare Programmierung funktioniert gut, wenn die Module gut miteinander kombiniert „verklebt“ werden können. Funktionale Programmierung bietet gute „Kleber“ die seine Module (also Funktionen) zu neuen Modulen (Funktionen) zu kombinieren („verkleben“) „This is the key to functional programming's power - it allows greatly improved modularisation. It is also the goal for which functional programmers must strive - smaller and simpler and more general modules, glued together with the new glues we shall describe.“ aus John Hughes: Why Functional Programming Matters Seite 11 Funktionale Programmierung: Was ist das und wozu gibt es das? Kleber 1: Kombination von Funktionen zu neuen Funktionen In der funktionalen Programmierung sind die Funktionen höherer Ordnung essentiell. Insbesondere Kombinatoren wie – map – reduce / fold – unfold – ... „The first of the two new kinds of glue enables simple functions to be glued together to make more complex ones.“ aus John Hughes: Why Functional Programming Matters Seite 12 Funktionale Programmierung: Was ist das und wozu gibt es das? Kleber 2: Kombination von Programmen zu neuen Programmen In der funktionalen Programmierung sind es essentiell, dass – die Ströme, also (potentiell) unendliche Folgen von Werten, – bedarfsgetrieben verarbeitetet werden können. Denn dass erlaubt die Kombinationen von „Programmen“ zu neuen „Programmen“. „The other new kind of glue that functional languages provide enables whole programs to be glued together. Recall that a complete functional program is just a function from its input to its output. If f and g are such programs, then (g . f) is a program which, when applied to its input, computes g (f input). [...] The two programs f and g are run together in strict synchronisation. F is only started once g tries to read some input, and only runs for long enough to deliver the output g is trying to read. Then f is suspended and g is run until it tries to read another input.“ aus John Hughes: Why Functional Programming Matters Seite 13 Funktionale Programmierung: Was ist das und wozu gibt es das? Funktionale Programmierung, die Essenz Nach John Hughes: Why Functional programming Matters: – Funktionen höherer Ordnung – Lazy Evaluation „In this paper we show that two features of functional languages in particular, higher-order functions and lazy evaluation, can contribute significantly to modularity.“ aus John Hughes: Why Functional Programming Matters Seite 14 Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Map: Eine Funktion auf alle Elemente einer Liste anwenden def doubleAll(lst: List[Int]) : List[Int] = lst match { case Nil => Nil case h :: t => 2*h :: doubleAll(t) } def tripleAll(lst: List[Int]) : List[Int] = lst match { case Nil => Nil case h :: t => 3*h :: tripleAll(t) } Verallgemeinern def map(f:Int => Int)(lst: List[Int]) : List[Int] = lst match { case Nil => Nil case h :: t => f(h) :: map(f)(t) } Spezialisieren val doubleAll = map(x => 2*x) _ val tripleAll = map(x => 3*x) _ Seite 15 Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Map in Scala: Eine Methode die von allen Kollektionen angeboten wird def map(f:Int => Int) : List[Int] => List[Int] = lst => lst match { case Nil => Nil case h :: t => f(h) :: map(f)(t) } val doubleAll = map(x => 2*x) val tripleAll = map(x => 3*x) assert( List(1,2,3).map(x => 2*x) == doubleAll(List(1,2,3)) ) assert( List(1,2,3).map(_*3) == tripleAll(List(1,2,3)) ) List(1,2,3).map(_*3) == List(1,2,3).map( x => x*3 ) Seite 16 Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Reduce : Eine Struktur (z.B. eine Liste) zu einem Wert reduzieren def sumAll(lst: List[Int]) : Int = lst match { case Nil => throw new IllegalArgumentException case h :: Nil => h case h :: t => h + sumAll(t) } def multAll(lst: List[Int]) : Int = lst match { case Nil => throw new IllegalArgumentException case h :: Nil => h case h :: t => h * multAll(t) } Verallgemeinern def reduce(f:(Int, Int) => Int) : List[Int] => Int = lst => lst match { case Nil => throw new IllegalArgumentException case h :: Nil => h case h :: t => f(h, reduce(f)(t)) } Spezialisieren val sumAll = reduce( _ + _ ) val multAll = reduce( _ * _ ) Seite 17 Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Fold : Eine Struktur (z.B. eine Liste) zu einem Wert zusammen-falten (reduce mit Startwert, neutrales Element der Operation) def sumAll(lst: List[Int]) : Int = lst match { case Nil => 0 case h :: t => h + sumAll(t) } def multAll(lst: List[Int]) : Int = lst match { case Nil => 1 case h :: t => h * multAll(t) } Verallgemeinern def fold(start: Int)(f:(Int, Int) => Int) : List[Int] => Int = lst => lst match { case Nil => start case h :: t => f(h, fold(start)(f)(t)) } Spezialisieren val sumAll = fold(0)( _ + _ ) val multAll = fold(1)( _ * _ ) Seite 18 Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Fold : Von rechts oder links falten def foldRight[T](start: T)(f:(T, T) => T) : List[T] => T = lst => lst match { case Nil => start case h :: t => f(h, foldRight(start)(f)(t)) } (a+(b+(c+""))) val concatR = foldRight("\"\"")("(" + _ + "+" + _ + ")") println(concatR(List("a", "b", "c"))) def foldLeft[T](start: T)(f:(T, T) => T) : List[T] => T = lst => lst match { case Nil => start case h :: t => foldLeft(f(start, h))(f)(t) } val concatL = foldLeft("\"\"")( "(" + _ + "+" + _ + ")") println(concatL(List("a", "b", "c"))) Seite 19 (((""+a)+b)+c) Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Fold und reduce in Scala : Methoden von Kollektionstypen println( List("a", "b", "c"). foldRight("\"\"")("(" + _ + "+" + _ + ")") ) println( List("a", "b", "c"). foldLeft("\"\"")("(" + _ + "+" + _ + ")") ) Seite 20 (a+(b+(c+""))) (((""+a)+b)+c) Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Flatmap : Eine Struktur „mappen“ und dann „flach klopfen“ val lst: List[List[Int]] = List(List(1), List(2,3), List(4, 5, 6)) println( lst.map { x:List[Int] => x.map(_*2) } ) List(List(2), List(4, 6), List(8, 10, 12)) println( lst.flatMap { x:List[Int] => x.map(_*2) } ) List(2, 4, 6, 8, 10, 12) Seite 21 Funktionen höherer Ordnung Verallgemeinerte Algorithmen auf Datenstrukturen Weitere „Catamorphismen“: – zip – filter – ... Zwei Listen zu einer Liste von Paaren zippen Eine Liste mit einem Prädikat filtern val l1 = List(1, 2, 3, 4, 5) val l2 = List ("a", "b", "c", "d", "e") println( l1.zip(l2) ) println( l1.zip(l2).unzip ) println( l2.zipWithIndex ) // ~> List((1,a), (2,b), (3,c), (4,d), (5,e)) // ~> (List(1, 2, 3, 4, 5),List(a, b, c, d, e)) // ~> List((a,0), (b,1), (c,2), (d,3), (e,4)) println( l1.reverse ) // ~> List(5, 4, 3, 2, 1) println( l1.filter( _ % 2 == 0) ) // ~> List(2,4) Seite 22 Funktionen höherer Ordnung Beispiel Quicksort – funktional def quicksort(lst: List[Int]) : List[Int] = if (lst.length <= 1) lst else { val pivot = lst(0) val small = lst.filter { _ < pivot } val mid = lst.filter { _ == pivot } val large = lst.filter { _ > pivot } quicksort(small) ::: mid ::: quicksort(large) } Seite 23 Funktionen höherer Ordnung Beispiel Permutationen – funktional def perms[T](l: List[T]) : List[List[T]] = l match { case Nil => List(Nil) case x::ll => (perms(ll) flatMap( ins(x, _) )) } def ins[T](x: T, l: List[T]) : List[List[T]] = l match { case Nil => List(List(x)) case a::rl => (x::l) :: (ins(x, rl) map(a :: _)) } println( perms(List("a", "b", "c") ) map ( _ reduce(_+_) ) ) List(abc, bac, bca, acb, cab, cba) Seite 24 Funktionen höherer Ordnung For-Ausdrücke (For-Comprehension) For-Ausdrücke – auch For-Comprehension oder speziell List-Comprehension – For als Ausdruck – statt als Anweisung – Findet sich in allen Sprachen mit funktionalen Ausdrucksmitteln Z.B: als List-Comprehension in Haskell und in Python: ein Ausdruck der eine Listen-Verarbeitung – verallgemeinert und angereichert in Scala For-Ausdrücke in Scala – Allgemeine Form for ( Generator- / Filter- / Definitions-Sequenz ) yield Ausdruck Seite 25 Funktionen höherer Ordnung For-Ausdrücke in Scala Beispiel 1 case class Person(name: String, isFemale: Boolean, children: Person*) val lara = Person("Lara", true) val hugo = Person("Hugo", false) val nadja = Person("Nadja", true, lara, hugo) val karla = Person("Karla", true, nadja) val emil = Person("Emil", false, lara, hugo) val persons = List(lara, hugo, nadja, karla, emil) val motherAndChild = for(p <- persons; if p.isFemale; c <- p.children ) yield (p.name, c.name) Generator Filter Generator println(motherAndChild) List((Nadja,Lara), (Nadja,Hugo), (Karla,Nadja)) Seite 26 Funktionen höherer Ordnung For-Ausdrücke in Scala Beispiel 3 Ein Generator kann sich auf einen generierten Wert beziehen val lst = List(1, 2, -2, 3, 6, -4); val evenPos = for (va <- lst; vb <- List(va % 2) Generator Generator ) yield vb == 0 println(evenPos) List(false, true, true, false, true, true) Liste mit Seite 27 Funktionen höherer Ordnung For-Ausdrücke in Scala Hinter den Kulissen For-Ausdrücke basieren auf – map– flatMap- und – filter-Methoden val lst = List(1, 2, -2, 3, 6, -4); val evenPos_0 = for (va <- lst; vb <- List(va % 2) ) yield vb == 0 println(evenPos_0) äquivalent val evenPos_1 lst.flatMap List(va % vb == 0 = { va => 2) } . map { vb => } println(evenPos_1) List(false, true, true, false, true, true) For-Ausdrücke werden vom Compiler in die zweite Form übersetzt Seite 28 List(false, true, true, false, true, true) Funktionen höherer Ordnung For-Ausdrücke in Scala For-Ausdrücke kooperieren mit Datentypen Damit der Compiler die for-Ausdrücke in Aufrufe von Map- / flatMap- und filter-Methoden übersetzen kann, müssen die – Generator-Ausdrücke Werte bezeichnen – die Instanzen von Typen sein, – die diese Methoden unterstützen Wir nennen sie „volkstümlich“ und informell monadische Typen Seite 29 Lazy Evaluation Rekursive Werte Rekursive Funktionen Die rekursive Definition von Funktionen ist stets völlig unproblematisch Auch in funktionalem Kontext, in dem Funktionen nur eine bestimmte Art von Werten darstellen val f: Int => Int = x => if (x==0) 1 else f(x-1)*x println(f(10)) Rekursive Werte im Allgemeinen können dagegen nicht definiert werden: val zeros: List[Int] = 0 :: zeros println(zeros) Exception in thread "main" java.lang.NullPointerException Woher kommt der Unterschied? Seite 30 Lazy Evaluation Rekursive Funktions- und andere Werte-Definitionen Rekursion Rekursion ist potentielle Unendlichkeit / Unbeschränkte Verfügbarkeit bei Bedarf val f: Int => Int = x => if (x==0) 1 else f(x-1)*x f wird beliebig oft, aber nur bei Bedarf ausgewertet zeros wird sofort (zum null-Pointer) ausgewertet val zeros: List[Int] = 0 :: zeros Rekursion und Auswertungsstrategie: Strikt / nicht strikt Der Unterschied zwischen rekursiven Funktionsdefinitionen und anderen rekursiven WertDefinitionen liegt an der unterschiedlichen Strategie bei der Auswertung der Ausdrücke x => if (x==0) 1 else f(x-1)*x if ist nicht strikt (lazy) in jeder Sprache 0 :: zeros :: und und andere Konstruktoren sind i.d.R. strikt (eager) Seite 31 Lazy Evaluation Rekursive Wertdefinitionen und Striktheit Emulation nicht-strikter Daten-Konstruktoren – Rekursive Wertdefinitionen – und damit (potentiell) unendliche Werte – sollten sich darum mit nicht-strikten Datenkonstruktoren definierten lassen. – Datenkonstruktoren sind üblicherweise strikt – Nicht-strikte Datenkonstruktoren können aber (auf diverse Arten) emuliert werden Beispiel : Emulation von def numbersFrom(n: Int): List[Int] = n :: numbersFrom(n+1) val numbers = numbersFrom(0) Exception in thread "main" java.lang.StackOverflowError Die Auswertung des zweiten Parameters dieser Konstruktor-Funktion (traditionell „cons“ genannt) muss verzögert werden. Seite 32 Lazy Evaluation Rekursive Wertdefinitionen und Striktheit Emulation nicht-strikter Daten-Konstruktoren durch die Verwendung von parameterlosen Funktions-Parametern („Thunks“) abstract class MyList { val head: Int val tail: () => MyList } Parameterlose Funktion statt Wert, oft „Thunk“ genannt, dient hier der Verzögerung der Auswertung. (Funktionen werden stets unausgewertet übergeben.) case object Nil extends MyList { val head: Nothing = { throw new IllegalAccessException } val tail: Nothing = { throw new IllegalAccessException } } case class Cons(n: Int, l: ()=>MyList) extends MyList { val head: Int = n val tail: () => MyList = l } object MyList { def cons(n: Int, l: ()=>MyList) : MyList = new Cons(n, () => l()) } Ein Listen-Typ mit nichtstrikter cons-Operation. cons-Operation def numbersFrom(n: Int) : MyList = MyList.cons(n, () => numbersFrom(n+1)) var numbers : MyList = numbersFrom(0) def numbersFrom(n: Int): List[Int] = n :: numbersFrom(n+1) val numbers = numbersFrom(0) Seite 33 Lazy Evaluation Rekursive Wertdefinitionen und Striktheit Emulation nicht-strikter Daten-Konstruktoren Scala: Kontrolle über die Striktheit / Faulheit mit lazy val's und Namens-Parametern abstract class MyList { val head: Int lazy val tail: MyList = null } object Nil extends MyList { override val head: Int = { throw new IllegalAccessException } override lazy val tail: MyList = { throw new IllegalAccessException } } In Haskell (einer rein-funktionalen Spache) sind alle Operationen nicht-strikt. Rekursive Wertdefinitionen funktionieren darum „automatisch“. class Cons(n: Int, l: =>MyList) extends MyList { override val head: Int = n override lazy val tail: MyList = l } object MyList { def cons(n: Int, l: =>MyList) : MyList = new Cons(n, l) } var numbers : MyList = numbersFrom(0) var i = 0 while(i < 10) { println(numbers.head) numbers = numbers.tail i = i+1 } def numbersFrom(n: Int) : MyList = MyList.cons(n, numbersFrom(n+1)) Seite 34 Lazy Evaluation Strom / Stream Definition Ein Strom (Stream) ist eine unendliche Sequenz (Liste) von Werten Ströme und funktionale Programmierung I Ströme sind ein wichtiges software-technisches Mittel der funktionalen Programmierung: – Definiere Sequenzen über deren Bildungsgesetz, ohne schon gleich eine Länge anzugeben – Kombiniere Sequenzen mit Funktionen und / oder anderen Sequenzen – Am Schluss bestimme wenn nötig die Länge FizzBuzz-Beispiel *: object FizzBuzz extends App { def numsFrom(n: Int) : Stream[Int] = n #:: numsFrom(n+1) allNums G fizzBuzz hasChar OrElse take 30 Wertefolge definieren def hasCharsOrElse(s: String, n: Int): String = if (s.length>0) s else n.toString() def fizzBuzz(n: Int) : String = (if (n%3 == 0) "fizz" else "") + (if (n%5 == 0) "buzz" else "") Funktion auf den Werten als Kombination einfacher Funktionen definieren def G(n: Int) : String = hasCharsOrElse(fizzBuzz(n), n) val allNums = numsFrom(1) Wieviele werden benötigt val fizzBuzzes = allNums map (G) fizzBuzzes.take(30).foreach { println(_) } * https://en.wikipedia.org/wiki/Fizz_buzz } Seite 35 Lazy Evaluation Strom / Stream Ströme und funktionale Programmierung II Ströme sind ein wichtiges programmier-technisches Mittel der rein funktionalen Programmierung: – In rein funktionalen Sprachen können Datenstrukturen nur über (rekursive) Funktionen erzeugt werden. – Eine unbeschränkt große (potentiell unendliche) Struktur kann nicht in der üblichen Art der imperativen Programmierung erzeugt werden. – Eine alternative Methode muss darum angeboten werden Die einfachste und sauberste Lösung ist die von Haskell: Alle Operationen sind nicht-strikt (lazy) Potentiell unendliche Strukturen sind dann kein Sonderfall, sondern ergeben sich in natürlicher Art Allerdings ist die Implementierung aufwändiger und die durchgängige Faulheit wird mit einer geringeren Effizienz bezahlt Seite 36 Lazy Evaluation Strom / Stream Scala und rekursive Datenstrukturen (potentiell unendliche Ströme) Scala bietet – die Mittel um die Striktheit von Operationen zu beeinflussen. (by-name-Parameter, lazy) Potentiell unendliche (rekursive) Datenstrukturen können darum explizit in der „funktionalen Art“ definiert werden. – alle üblichen imperativen Konstrukte Potentiell unendliche (rekursive) Datenstrukturen können darum objektorientiert definiert werden – eine spezielle Datenstruktur Stream Potentiell unendliche (rekursive) Datenstrukturen können damit einfach in rein-funktionaler Form definiert werden. Streams in Scala sind leicht zu benutzen und sollten in Software-Komponenten, die im funktionalen Stil geschrieben werden, stets (selbst implementierten) Alternativen (thunk, by-name, lazy) vorgezogen werden. Seite 37 Lazy Evaluation Strom-Beispiele Klassiker der Strom-Programmierung 1 : Generiere den Strom der Fibonacci-Zahlen def fib(v1: Int, v2: Int) : Stream[Int] = v1 #:: fib(v2, v1+v2) val fibs: Stream[Int] = fib(0, 1) fibs.take(20) foreach { println(_) } Klassiker der Strom-Programmierung 2 : Generiere alle Primzahlen mit dem Sieb des Eratosthenes def natsFrom(n: Int): Stream[Int] = n #:: natsFrom(n+1) val nats: Stream[Int] = natsFrom(2) def sieve(s: Stream[Int]): Stream[Int] = s match { case h #:: t => h #:: sieve(t).filter( _ % h != 0) } sieve(nats).take(20) foreach { println(_) } Seite 38 Lazy Evaluation Ströme und Unfold Unfold: einen Wert zu einer Datenstruktur entfalten – Beispiel, generischer Strom: def unfold[T, S](s: S)(g: S => Option[(T, S)]): Stream[T] = case None => Stream() case Some((t, s1)) => t #:: unfold(s1)(g) } for (x <- unfold(1)(x => Some((x, x+1))) take 9 ) { println(x) } 1 2 3 4 5 6 7 8 9 0 1 1 2 3 5 8 13 21 34 for (fib <- unfold( (0,1) ) { case (x, y) => Some((x, (y, x+y)) ) } take 10 ) { } g(s) match { println(fib) Seite 39