Funktionale Programmierung Th. Letschert TH Mittelhessen Gießen University of Applied Sciences Iteratoren, Generatoren, Ströme – Iterierbare und Traversierbare Datenstrukturen – Generatoren – rekursiv definierte Datenstrukturen, Ströme – Ströme und Unfold – Beispiel Permutationen Iterieren und Traversieren Iteratoren Iterator-Konzept: Durchlaufen einer Kollektion deren Struktur geheim ist Iteratoren sind ein Idiom (Muster) zum Durchlaufen aller Elemente einer Kollektion, ohne dass deren interne Struktur dazu bekannt sein muss. Die Logik des Durchlaufens der Kollektion wird dazu in einer Iterator-Klasse realisiert Die Implementierung der Iterator-Klasse ist ein Geheimnis der Kollektionsklasse Iteratoren sind also eine Anwendung des Geheimnisprinzips Iteratoren sind ein imperatives Konzept Ein Iterator ist seinem Wesen nach veränderlich: Jeder Aufruf von next „schiebt den Iterator an eine neue Position“ und verändert ihn damit. Iteratoren sind allgegenwärtig Jede moderne Sprache mit einer Unterstützung von Kollektionstypen bietet Iteratoren. Iterator-Varianten Iteratoren können in unterschiedlicher Art realisiert werden: Itertor / next liefert einen Wert oder eine Referenz Iteratoren unterstützen die Manipulation der Kollektion oder unterstützen sie nicht ... Seite 2 Iterieren und Traversieren Iteratoren Iteratoren: Externes Durchlaufen einer Kollektion unter Kontrolle des Anwenders Bei der Verwendung von Iteratoren wechselt die Kontrolle zwischen – dem Iterator (hasNext / next) und damit der Kollektion (der Iterator ist Bestandteil der Kollektion), und – dem Anwender Der Iterator muss darum den aktuellen Zustand der Traversierung der Kollektion von Aufruf zu Aufruf von next / hasNext speichern. Genau genommen ist ein Iterator ein Speicher für den Zustand einer Traversierung. Die Kollektion mit ihrem Iterator und der Anwender bilden ein System von Coroutinen: d.h. ein einfaches nebenläufiges System. Kontrolle Iterator Probleme der Kombination von Zustand und Nebenläufigkeit spielen darum eine Rolle: Was passiert, wenn der Anwender (oder sonst jemand) die Kollektion verändert, die er gerade mit einem Iterator durchläuft. next / hasNext Kollektion Anwender Iterieren Seite 3 Iterieren und Traversieren Traversieren Traversieren: Internes Durchlaufen unter der Kontrolle der Kollektion Traversierbare Kollektionen haben eine Traversierungsmethode, – die es erlaubt die Kollektion ohne die Kenntnis ihrer Struktur zu durchlaufen, – dabei wird die Kontrolle komplett an die Kollektion übergeben Der Anwender übergibt eine Aktion / Funktion, die unter der Kontrolle der Kollektion auf alle Elemente angewendet wird. Anwender und Kollektion sind nicht verschränkt (nebenläufig) aktiv. Kontrolle Probleme der Nebenläufigkeit treten nur dann eventuell auf, wenn mehrere Threads ins Spiel kommen. Aktion Kollektion Anwender Traversieren Seite 4 Iterieren und Traversieren Traversieren vs Iterieren Traversieren ist einfacher als Iterieren Da beim Traversieren die Wechsel der Kontrolle entfallen, muss der Zustand des Durchlaufens nicht von Aufruf zu Aufruf gespeichert werden. (Eine Implementierung von Coroutinen ist nicht notwendig) Traversierbar ist darum meist wesentlich einfacher zu realisieren als Iterierbar. Beispiel: Traversieren von Binärbäumen. sealed abstract class BinTree case class Leaf(v: Int) extends BinTree case class Node(left: BinTree, right: BinTree) extends BinTree def traverse(tree: BinTree, action: Int => Unit) : Unit = tree match { case Leaf(v) => action(v) case Node(t1, t2) => { traverse(t1, action); traverse(t2, action) } } val tree = Node(Node(Leaf(1), Leaf(2)), Node(Leaf(3), Leaf(4))) traverse(tree, i => println(i)) Seite 5 Traversieren: Triviale Rekursion über die Struktur der Kollektion Iterieren und Traversieren Traversieren vs Iterieren Beispiel: Traversieren von Binärbäumen – Objektorientiert / generisch. sealed abstract class BinTree extends Traversable[Int] case class Leaf(v: Int) extends BinTree { def foreach[U](action: Int => U) : Unit = action(v) } case class Node(left: BinTree, right: BinTree) extends BinTree { def foreach[U](action: Int => U) : Unit = { left.foreach(action); right.foreach(action) } } val tree = Node(Node(Leaf(1), Leaf(2)), Node(Leaf(3), Leaf(4))) tree.foreach((i:Int) => println(i)) Seite 6 Iterieren und Traversieren Traversieren vs Iterieren Iterieren über Binärbäume I Ein Iterator entspricht der Zustandsinformation einer unterbrochenen Traversierung. Die Unterbrechung ist notwendig, da die Kontrolle regelmäßig zwischen der Traversierung und ihrem Nutzer wechselt. Der Algorithmus ist rekursiv, nutzt also implizit den Laufzeitstack der Implementierungssprache zur Verwaltung seines Zustands. Um die Zustände offen zu legen, muss darum zunächst die Rekursion entfernt werden. def traverseLoop(tree: BinTree, action: Int => Unit): Unit = { var pending: List[BinTree] = List() var actTree: Option[BinTree] = Some(tree) } while (actTree.isDefined) { actTree.get match { case Leaf(v) => action(v) pending match { case Nil => actTree = None case h :: t => actTree = Some(h) pending = t } case Node(t1, t2) => actTree = Some(t1) pending = t2 :: pending } } Rekursion mit einem Stapel abwickeln: Nach links absteigen, rechte Unterbäume in pending stapeln def traverse(tree: BinTree, action: Int => Unit) : Unit = tree match { case Leaf(v) => action(v) case Node(t1, t2) => { traverse(t1, action); traverse(t2, action) } } Seite 7 Iterieren und Traversieren Traversieren vs Iterieren Iterieren über Binärbäume II Schleife unterbrechbar machen Bedingung ~> hasNext / Körper ~> next def iterator(tree: BinTree): Iterator[Int] = { var pending: List[BinTree] = List() var actTree: Option[BinTree] = Some(tree) new Iterator[Int] { def hasNext: Boolean = actTree.isDefined def next: Int = { while (!actTree.get.isInstanceOf[Leaf]) { val Node(t1, t2) = actTree.get.asInstanceOf[Node] actTree = Some(t1) pending = t2 :: pending } val result = actTree.get.asInstanceOf[Leaf].v def traverseLoop(tree: BinTree, action: Int => Unit): Unit = { pending match { var pending: List[BinTree] = List() case Nil => actTree = None var actTree: Option[BinTree] = Some(tree) case h :: t => while (actTree.isDefined) { iter.hasNext() actTree = Some(h) actTree.get match { case Leaf(v) => pending = t action(v) action (iter.next()) } pending match { result case Nil => actTree = None } case h :: t => } actTree = Some(h) } Seite 8 } } } pending = t } case Node(t1, t2) => actTree = Some(t1) pending = t2 :: pending Iterieren und Traversieren Traversieren vs Iterieren Zusammenfassung – Eine externe Traversierung mit einem Iterator ist komplexer als eine interne Traversierung, da der Zustand der Traversierung vom Programm explizit und ohne Hilfe des Laufzeit-stacks (also ohne Rekursion) verwaltet werden muss. – Ein externe Traversierung mit einem Iterator ist gefährlicher / verwundbarer / weniger funktional als eine interne Traversierung, da der Zustand der Traversierung explizit im Programm verfügbar ist und darum das Opfer unachtsamer Manipulationen werden kann. Seite 9 Generieren Generator Ein Generator ist ein Iterator über eine Datenstruktur, die er selbst „im Fluge“ erzeugt. Ein Iterator ist ein Generator, der sich durch eine Datenstruktur hangelt. Generator-Konzept – Ein Generator ist ein Mechanismus zur Erzeugung einer Wertefolge – Generatoren entsprechen Iteratoren Die Kontrolle wechselt zwischen Generator und seinem Anwender Ein Generator hat einen Zustand – Generatoren teilen den Coroutinen-Charakter mit Iteratoren – Iteratoren können als Speziallfall der Generatoren angesehen werden. Generator Kontrolle Anwender next / hasNext Seite 10 Generieren Generator Generator-Einsatz Die – Erzeugung einer Datenstruktur und – die Verwendung ihrer Elemente werden verschränkt: – zur Reduktion des Speicherbedarfs – weil nur ein (nicht vorhersehbarer) Teil der Daten gebraucht wird – weil die Datenstruktur unendlich ist und es unmöglich ist, sie komplett zu erzeugen Seite 11 Generieren Generator Beispiel Generator als Iterator über eine Datenstruktur, die er im Fluge selbst erzeugt case object NaturalNumbers { def upTo(n: Int) : Iterable[Int] = new Iterable[Int] { def iterator: Iterator[Int] = new Iterator[Int] { var i: Int = 0 def hasNext: Boolean = i <= n def next: Int = { val res = i i = i+1 res } } } } for (x <- NaturalNumbers upTo 10 ) { println(x) } Seite 12 Generieren Generator-Implementierung: als Coroutine Generatoren in Python Die Programmiersprache Python bietet Generatoren als Sprachkonstrukt an. Generatoren werden dort durch unterbrechbare Funktionen realisiert. Ein Funktion liefert mit yield (statt return) einen Wert und bei einem erneuten Aufruf geht es an an der unterbrochen Stelle im unterbrochenen Zustand weiter Beispiel: Seite 13 Generieren Generator-Implementierung als Coroutine Warum Generatoren als Coroutinen Für triviale Sequenzen – ist es einfach einen Iterator zu schreiben und genauso – ist es einfach einen Generator zu schreiben Komplexe Strukturen – benötigen eine komplexe Zustandsverwaltung – die bei einem Generator genau wie bei einem Iterator komplett von der Anwendung zu realisieren ist: kein Laufzeit-Stack ~> keine Rekursion! (Wechsel der Kontrolle zwischen Generator und Anwender) Mit einem Generator als Coroutine – können rekursive (Traversierungs-) Funktionen – leicht in Generatoren umgewandelt werden Generator = Iterator mit eigenem Stack! Seite 14 Generieren Generator-Implementierung als Coroutine Coroutinen mit Theads realisieren Beispiel: case object NumberGenerator { private var i: Int = 0 private var nums = new java.util.concurrent.ArrayBlockingQueue[Int](1) private def yielt(x: Int) : Unit = { nums.put(x) } new Thread(new Runnable{ override def run() : Unit = { while (true) { yielt(i); i = i+1 } } }) start } def upTo(n: Int) : Iterable[Int] = new Iterable[Int] { def iterator: Iterator[Int] = new Iterator[Int] { def hasNext: Boolean = i <= n def next: Int = { nums.take } } } for (x <- NumberGenerator upTo 10) { println(x) } Seite 15 Coroutinen können mit (dem mächtigeren Konzept der) Threads implementiert werden. (Skizze, keine robuste Lösung) Generieren Generator-Implementierung mit Continuations – Der Generator muss in seinem Lauf unterbrechbar sein damit ein Wert und die Kontrolle an den Anwender übergeben werden kann – Nach einer Unterbrechung muss es an der unterbrochenen Stelle weiter gehen – Die Unterbrechnungsstelle kann durch eine Fortsetzungsfunktion repräsentiert werden: „der Rest der Berechnung ab hier“ – Manche Sprachen bieten den expliziten Umgang mit Continuations – Diese können genutzt werden um Coroutinen und damit auch Generatoren zu implementieren Seite 16 Rekursive Typen, Funktionen und Werte Rekursive Typen Rekursive Typen Rekursiver Typ: Ein Typ, der mit Hilfe von sich selbst definiert wird Rekursive Typen können in allen Sprachen in der Regel problemlos definiert werden Typischerweise als Summentyp mit einer rekursiven und einer nicht-rekursiven Variante: IntList = Nil | < Int, IntList > In Scala: abstract class IntList case object Nil extends IntList case class Cons(head: Int, tail: IntList) extends IntList Endliche Instanzen (Werte) dieses rekursiven „unendlichen“ Typs können problemlos erzeugt werden: val l_123: IntList = Cons(1, Cons(2, Cons(3, Nil))) Seite 17 Rekursive Typen, Funktionen und Werte Rekursive Typen Rekursive Funktions-Typen Die rekursive Definition von Funktionstypen wird üblicherweise nicht akzeptiert Scala: type F = Int => F illegal cycle involving type F type F = Function[Int, F] illegal cycle involving type F Instanzen dieses rekursiven Typs können jedoch problemlos definiert werden: class F extends Function[Int, F] { def apply(x: Int): F = new F } Eine Funktion mit dem rekursivem Typ F = Int => F val f = new F val f1 = f(1) val f2 = f1(2) object F extends Function[Int, F] { def apply(x: Int): Function[Int, F] = F } not found type F Seite 18 Rekursive Typen, Funktionen und Werte 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 19 Rekursive Typen, Funktionen und Werte 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 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 20 Rekursive Typen, Funktionen und Werte 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 21 Rekursive Typen, Funktionen und Werte 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 22 Rekursive Typen, Funktionen und Werte 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 23 Ströme 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 aus Foliensatz 1: 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(_) } } Seite 24 Ströme 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 25 Ströme 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 den Alternativen (thunk, by-name, lazy) vorgezogen werden. Seite 26 Ströme 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 27 Ströme 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 28 Strom vs Generator Strom vs Generator Ströme und Generatoren Ströme und Generatoren sind eng verwandt, aber nicht das gleiche. Oft kann ein Strom einen Generator ersetzen und umgekehrt. Beispiel Permutationen – Durchlaufe alle Permutationen einer Folge bis eine geeignete gefunden wurde. – Die Folge der Permutationen soll dabei „im Fluge“ generiert werden. Seite 29 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Teile-und-Herrsche-Algorithmus 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 :: _)) } Sortieren mit Permutationen: Finde die / eine Permutation in der die Elemente sortiert auftreten: println( perms(List(8,2,7,6,3) ) find ( _ . sliding(2) . forall { x => x(0) <= x(1) } ) ) Some(List(2, 3, 6, 7, 8)) Problem: Alle Permutationen werden vor dem ersten Test erzeugt, obwohl vielleicht die erste schon sortiert ist. Seite 30 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Teile-und-Herrsche-Algorithmus / Strom-Implemetierung def perms[T](l: List[T]) : Stream[List[T]] = l match { case Nil => Nil #:: Stream() case x::ll => (perms(ll) flatMap( ins(x, _) )) } def ins[T](x: T, l: List[T]) : Stream[List[T]] = l match { case Nil => List(x) #:: Stream() case a::rl => (x::l) #:: (ins(x, rl) map(a :: _)) } Seite 31 Strom, Iterator, Generator: Beispiel Permutationen Permutationen def perms[T](l: List[T]) : Stream[List[T]] = l match { case Nil => Nil #:: Stream() case x::ll => (perms(ll) flatMap( ins(x, _) )) } Generator- vs Strom-Implemetierung def ins[T](x: T, l: List[T]) : Stream[List[T]] = l match { case Nil => List(x) #:: Stream() case a::rl => (x::l) #:: (ins(x, rl) map(a :: _)) } Scala / Strom-Implementierung Python / Generator-Implementierung* *aus Th. Letschert: Nichtprozedurale Programmierung in Python und anderen Sprachen. Seite 32 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Problem der Divide-and-Conquer-basierten Generator- und der Strom-Implemetierung Beide Varianten, die Strom und die Generator-Version sind nicht das was wir wirklich wollen: Der Divide-and-Conquer-Algorithmus – egal in welche Variante – erzeugt die Permutationen der Länge n aus den Permutationen der Länge n-1. D.h. – bevor die erste Permutation der der Länge n erzeugt wird, – werden alle Permutationen der Länge n-1 erzeugt. Immerhin müssen nicht alle Permutationen der Länge erzeugt werden, bevor die erste geprüft werden kann. Seite 33 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Iterativer Algorithmus: Filtere alle Tupel Ein einfacher iterativer Algorithmus* geht von der Beobachtung aus, dass – die Permutationen einer Folge <s1, s2, .. sk> der Länge k – eine Teilmenge der Tupel der Länge k von Elementen der Menge {s1, s2, .. sk} sind Sind alle Elemente von {s1, s2, .. sk} unterschiedlich – dann sind die Permutationen die Tupel ohne Wiederholung Beispiel Alle Permutationen von Listen der Länge 3: def tuples3[T](l: List[T]): List[List[T]] = for (i <- l; j <- l; k <- l) yield (i :: j :: k :: Nil ) Bei dieser iterativen Generierung der Tupel muss die Länge der Liste (hier 3) statisch bekannt sein. def perms3[T](l: List[T]): List[List[T]] = tuples3(l) filter ( l => l.distinct.size == l.size ) perms3(List('a, 'b, 'c)).foreach { println(_) } List('a, List('a, List('b, List('b, List('c, List('c, 'b, 'c, 'a, 'c, 'a, 'b, 'c) 'b) 'c) 'a) 'b) 'a) Seite 34 * eine tiefgehende Diskussion der Thematik findet sich in: D. Knuth A Draft of section 7.2.1.2: Generating all Permutations; http://www-cs-faculty.stanford.edu/~uno/taocp.html Strom, Iterator, Generator: Beispiel Permutationen Permutationen Erzeuge alle Tupel – Nach dem „Zählerprinzip“ Ein einfacher iterativer Algorithmus zur Erzeugung aller Tupel der Länge k, bei statisch unbekanntem k, kann als „Zähler“ implementiert werden. Die „Ziffern“ sind dabei Indexpositionen der gegeben Liste. Die Tupel werden als Tupel von Indexpositionen berechnet. Beispiel: 00 01 10 11 def allTuples[T](k: Int, l: List[T]) : Unit = { import scala.collection.mutable.Buffer // the index positions var actual : Buffer[Int] = Buffer.fill(k)(0) } var j = k-1 var stop = false while (!stop) { println( actual.map(l(_)) ) // indices to values j = k-1 while (j >= 0 && actual(j) == l.length-1) { actual(j) = 0 j = j-1 } if (j>=0) { actual(j) = actual(j)+1 } else { stop = true } } allTuples(2, List('a, 'b)) Seite 35 ArrayBuffer('a, ArrayBuffer('a, ArrayBuffer('b, ArrayBuffer('b, 'a) 'b) 'a) 'b) Strom, Iterator, Generator: Beispiel Permutationen Permutationen Erzeuge alle Tupel iterativ und filtere Duplikate aus def allTuples[T](k: Int, l: List[T]) : List[List[T]] = { import scala.collection.mutable.Buffer val result: Buffer[List[T]] = Buffer() // collect the tuples var actual : Buffer[Int] = Buffer.fill(k)(0) // the index positions var j = k-1 var stop = false while (!stop) { result += actual.map(l(_)).toList // indices to values j = k-1 while (j >= 0 && actual(j) == l.length-1) { actual(j) = 0 j = j-1 } if (j>=0) { actual(j) = actual(j)+1 } else { stop = true } } } result.toList def perms[T](l: List[T]) : List[List[T]] = { val indices = Range(0, l.length).toList val tuplesOfIndices = allTuples(indices.length, indices) val permsOfIndices = tuplesOfIndices.filter( tuple => tuple.distinct.size == tuple.size ) permsOfIndices.map( _ map(l(_) ) ) } Seite 36 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Tupel-Iterator Aus dem iterativen Tupel-Generator kann leicht ein Iterator entwickelt werden: case class AllTuples[T](k: Int, s: List[T]) extends Iterable[List[T]] { import scala.collection.mutable.Buffer var a : Buffer[Int] = Buffer.fill(k)(0) var j = k-1 var stop = false def iterator = new Iterator[List[T]] { override def hasNext(): Boolean = !stop } override def next(): List[T] = { val res = a.map( s(_) ).toList j = k-1 while (j >= 0 && a(j) == s.length-1) { a(j) = 0 j = j-1 } if (j>=0) { a(j) = a(j)+1 } else { stop = true } res } } Seite 37 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Permutationen-Iterator Der Tupel-Iterator kann von einem Permutations-Iterator genutzt werden: case class Perms[T](l: List[T]) extends Iterable[List[T]] { val indices = Range(0, l.length).toList val tupleIter = AllTuples(indices.length, indices).iterator var tupleBuffer: Option[List[T]] = None def iterator = new Iterator[List[T]] { override def hasNext(): Boolean = { while (tupleBuffer == None && tupleIter.hasNext) { val nextTuple = tupleIter.next tupleBuffer = if (nextTuple.distinct.size == nextTuple.size) Some(nextTuple.map(l(_))) else None } tupleBuffer.isDefined } } override def next(): List[T] = { val res = tupleBuffer.get tupleBuffer = None res } } Seite 38 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Permutationen-Iterator: Problem Der Tupel-Iterator und der Permutations-Iterator sind zustandsbehaftet und „nebenläufig“ aktiv. Sie müssen synchronisiert werden. Korrektheit der notwendigen Synchronisation unklar case class Perms[T](l: List[T]) extends Iterable[List[T]] { val indices = Range(0, l.length).toList val tupleIter = AllTuples(indices.length, indices).iterator var tupleBuffer: Option[List[T]] = None def iterator = new Iterator[List[T]] { override def hasNext(): Boolean = { while (tupleBuffer == None && tupleIter.hasNext) { val nextTuple = tupleIter.next tupleBuffer = if (nextTuple.distinct.size == nextTuple.size) Some(nextTuple.map(l(_))) else None } tupleBuffer.isDefined } } override def next(): List[T] = { val res = tupleBuffer.get tupleBuffer = None res } } Seite 39 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Permutationen-Generator Ein Generator für Permutationen hat die gleiche Problematik Tupel-Generator und Permutations-Generator müssen in der gleichen Art synchronisiert werden. Seite 40 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Tupel-Strom Ein Tupel-Strom kann mit der gleichen „Zähler-Logik“ erzeugt werden, wie der Tupel-Iterator def unfold[T, S](s: S)(g: S => Option[(T, S)]): Stream[T] = case None => Stream() case Some((t, s1)) => t #:: unfold(s1)(g) } g(s) match { import scala.collection.mutable.Buffer def nextCounter(counter: List[Int]) : Option[(List[Int], List[Int])] = if (counter == List()) None else { var a : Buffer[Int] = Buffer.tabulate(counter.length)(i => counter(i)) var j = counter.length-1 } Wir verzichten darauf die Schleife in eine Rekursion umzuwandeln. Anhängern der funktionale Reinheit sei diese Umwandlung als Übungsaufgabe empfohlen. while (j >= 0 && a(j) == counter.length-1) { a(j) = 0 j = j-1 } if (j>=0) { a(j) = a(j)+1; Some((counter, a.toList)) } else Some((counter, List())) def allTuples[T](k: Int, l: List[T]): Stream[List[T]] = (unfold(List.fill(l.length)(0))( nextCounter )).map(_ map(l(_))) Seite 41 Strom, Iterator, Generator: Beispiel Permutationen Permutationen Permutations-Strom aus Tupel-Strom Aus dem Tupel-Strom kann der Strom der Permutationen durch Filtern erzeugt werden: def allPerms[T](l: List[T]): Stream[List[T]] = (unfold(List.fill(l.length)(0))( nextCounter )). filter( tuple => tuple.distinct.size == tuple.size map(_ map(l(_))) ) . Diese funktionale Lösung ist in der Tat übersichtlicher, als die imperative Lösung, bei der – ein Permuations-Iterator über einen – Tupel-Iterator definiert wurde! Sortieren: val l = List(1,2,3,4,5,6,7,8,9,10) println( "sorted: "+ allPerms(l) . find ( x => { println(x); x. sliding(2) . forall { x => x(0) <= x(1) } } ) ) stoppt sofort, wenn eine sortierte Permutation gefunden wurde. Achtung: es kann lange dauern bis das erste Tupel ohne Wiederholungen gefunden wird. Seite 42