Iteratoren, Generatoren, Ströme - Benutzer-Homepage

Werbung
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
Herunterladen