Foliensatz 5 (4 auf 1)

Werbung
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiele: (Funktionen auf Listen) (3)
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
5. Zusammenhängen der Elemente einer Liste von Listen:
concat :: [[a]] -> [a]
concat xl = if null xl then []
else append (head xl) ( concat (tail xl))
• Rekursive Funktionsdeklaration sind bei Listen angemessen, weil
Listen rekursive Datenstrukturen sind.
6. Wende eine Liste von Funktionen vom Typ Int -> Int
nacheinander auf eine ganze Zahl an:
• Mit Mustern lassen sich die obigen Deklaration noch eleganter
fassen (s. unten).
seqappl :: [( Int -> Int)] -> Int -> Int
seqappl xl i = if null xl then i
else seqappl (tail xl) (( head xl) i)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
241
3.1 Grundkonzepte funktionaler Programmierung
242
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstrukturen der Paare (2)
Wir betrachten zunächst Paare und verallgemeinern dann auf n-Tupel:
Paare oder 2-Tupel sind die Elemente des kartesischen Produktes
zweier ggf. verschiedener Mengen oder Typen. Der Typ der Paare ist
also ein Produkttyp. 2
Typ:
(a,b) , a, b sind Typparameter
Funktionen:
(==), (/=) :: (a, b) → (a, b) → Bool
wenn (==) auf a und b definiert
(_,_)
:: a → b → (a, b)
fst
:: (a, b) → a
snd
:: (a, b) → b
Als Typkonstruktor wird (a,b) in Mixfix-Schreibweise benutzt.
Konstanten:
Haskell stellt standardmäßig eine Datenstruktur für Paare bereit, die
bzgl. der Elementtypen parametrisiert ist.
TU Kaiserslautern
TU Kaiserslautern
3. Funktionales Programmieren
Die Datenstrukturen der Paare
©Arnd Poetzsch-Heffter
©Arnd Poetzsch-Heffter
keine
Dem Typ (a,b) ist die Menge der geordneten Paare mit Elementen
vom Typ a und b zugeordnet.
243
©Arnd Poetzsch-Heffter
TU Kaiserslautern
244
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Funktionen auf Paaren)
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der n-Tupel
Haskell unterstützt n-Tupel für alle n ≥ 3:
Transformiere eine Liste von Paaren in ein Paar von Listen:
Typ:
unzip :: [(a, b)] -> ([a], [b])
unzip xl =
if null xl then ([] , [])
else ( (fst (head xl)):(fst ( unzip (tail xl))),
(snd (head xl)):(snd ( unzip (tail xl))) )
Funktionen:
(==), (/=) :: (a, b, ...) → (a, b, ...) → Bool
wenn (==) auf a, b, ... definiert
(_,_,...) :: a → b → ... → (a, b, ...)
Konstanten:
it = unzip [(1 , 2), (3, 4) , (9, 10)]
(auch das geht erheblich schöner mit Mustern)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
(a,b,...) , a, b, ... sind Typparameter
keine
Seien n ≥ 3 und a1 , . . . , an Typen mit Wertemenge w(a1 ), . . . , w(an );
dann ist dem Tupeltyp (a1 , . . . , an ) das kartesische Produkt
w(a1 ) × · · · × w(an ) als Wertemengen zugorndet; also eine Menge
geordneter n-Tupel, wobei das i-te Element vom Typ ai ist.
245
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen:
246
3.1 Grundkonzepte funktionaler Programmierung
Die Datenstruktur der Einheit
Haskell unterstützt eine Datenstruktur mit einem definierten Wert:
Typ:
• Es gibt keine Funkionen, um Elemente aus einem Tupel zu
()
Funktionen:
selektieren. Dafür benötigt man Muster (siehe unten).
(==), (/=) :: () → () → Bool
• Paare sind wie 2-Tupel, auf ihnen sind aber die Selektorfunktionen
Konstanten:
fst und snd definiert.
()
• Es gibt keine 1-Tupel: Klammern um Ausdrücke dienen nur der
:: ()
Dem Typbezeichner () ist eine einelementige Wertemenge
zugeordnet. Der Wert wird als Einheit (engl. unity) bezeichnet.
Strukturierung und haben darüber hinaus keine Bedeutung; d.h.
wenn e ein Ausdruck ist, ist ( e ) gleichbedeutend mit e.
Bemerkung:
Die Einheit wird oft als Ergebnis verwendet, wenn es keine relevanten
Ergebniswerte gibt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
247
©Arnd Poetzsch-Heffter
TU Kaiserslautern
248
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Geschachtelte Tupel)
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (Funktionen auf n-Tupeln)
1. Flache ein Paar von Paaren in ein 4-Tupel aus:
Mit der Tupelbildung lassen sich „baumstrukturierte“Werte,
sogenannte Tupelterme, aufbauen. So entspricht der Tupelterm:
ausflachen :: ((a, b) ,(c, d)) -> (a, b, c, d)
-- nimmt ein Paar von Paaren und liefert 4-Tupel
ausflachen pp = ( fst (fst pp),
snd (fst pp),
fst (snd pp),
snd (snd pp) )
( (8,True), (("Tupel", "sind", "toll"), "aha"))
dem Baum:
it = ausflachen ( (True ,7) , (’x’ ,5.6) )
Alternative Deklaration mit Mustern:
True
8
"Tupel"
©Arnd Poetzsch-Heffter
ausflachen ((a, b) ,(c, d)) = (a, b, c, d)
"aha"
"sind"
TU Kaiserslautern
3. Funktionales Programmieren
2. Funktion zur Paarbildung:
"toll"
paarung :: a -> b -> (a,b)
paarung lk rk = (lk ,rk)
249
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Funktionstypen:
Beispiel:
Eine zweistellige Funktion f mit Argumenten vom Typ a und Typ b und
Ergebnistyp c kann in Haskell auf zwei Arten typisiert werden:
Die Additionsoperation (+) auf Typ Int hat in Haskell den Typ
1. Gecurryte Form:
(+) :: Int -> Int
f :: a -> b -> c
In der Mathematik typisiert man die Additionsoperation plus
üblichweise mit:
Nach den Präzedenzregeln für -> ist das a -> (b ->c),
also eine Funktion, die ein Wert vom Typ a nimmt und eine
Funktion vom Typ b -> c liefert.
plus :: (Int ,Int) -> Int
Diese Variante kann man in Haskell wie folgt definieren:
Ist x::a und y::b, dann sind (f x) y oder gleichbedeutend
f x y korrekte Anwendungen.
plus ip = (fst ip) + (snd ip)
2. Tupel-Form:
Oder eleganter mit Mustern:
f :: (a,b) -> c
plus (m,n) = m + n
In diesem Fall ist für x::a und y::b, f (x,y) eine korrekte
Anwendungen.
©Arnd Poetzsch-Heffter
250
TU Kaiserslautern
251
©Arnd Poetzsch-Heffter
TU Kaiserslautern
252
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
Muster in Deklarationen und Ausdrücken
Begriffsklärung: (Muster in Haskell)
Muster sind ein Sprachkonstrukt um strukturierte Werte einfacher
handhaben zu können (siehe Funktion ausflachen).
Muster (engl. Pattern) in Haskell sind Ausdrücke gebildet über
Bezeichnern, Konstanten und Konstruktoren.
Ein Wert heißt hier strukturiert, wenn er mittels Konstruktoren aus
anderen Werten zusammengebaut wurde.
Alle Bezeichner in einem Muster müssen verschieden sein.
Ein Muster M mit Bezeichnern b1 , . . . , bk passt auf einen
strukturierten Wert w (engl.: a pattern matches a value w), wenn es
eine Substitution der Bezeichner bj in M durch Werte vj gibt, in
Zeichen M[v1 /b1 , . . . , vk /bk ], so dass
Konstruktoren sind spezielle Haskell-Funktionen.
Bisher behandelte Konstruktoren:
• der Listkonstruktor (:) (daher der Name “cons”)
• die Tupelbildung durch (_,...,_)
M[v1 /b1 , . . . , vk /bk ] = w
In 3.1.4 werden wir benutzerdefinierte Konstruktoren kennen lernen.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
253
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (ML-Muster, Passen)
254
3.1 Grundkonzepte funktionaler Programmierung
Beispiel: (ML-Muster, Passen) (2)
5. first:rest passt auf ["computo","ergo","sum]
mit first ="computo" und rest =["ergo", "sum"] .
1. (x,y) passt auf (4,5) mit Substitution x=4, y=5.
6. ((8,x), (y,"aha")) passt
auf ((8,True), (("Tupel","sind","toll"), "aha"))
mit x = True und y = ("Tupel","sind","toll").
2. (erstesElem,zweitesElem) passt auf (-47,(True,"dada"))
mit erstesElem =-47, zweitesElem =(True,"dada") .
3. x:xs passt auf 7:8:9:[] mit x = 7 und xs = 8:9:[] , d.h.
xs = [8,9].
4. x1:x2:xs passt auf 7:8:9:[] mit x1 = 7, x2 = 8, xs = [9] .
8
©Arnd Poetzsch-Heffter
TU Kaiserslautern
255
©Arnd Poetzsch-Heffter
True
y
TU Kaiserslautern
"aha"
256
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Wertvereinbarungen mit Mustern
3.1 Grundkonzepte funktionaler Programmierung
Funktionsvereinbarung mit Mustern
Muster können in Haskell-Wertvereinbarungen verwendet werden:
<Muster >
=
<Ausdruck > ;
Muster können in Haskell-Funktionsdeklarationen verwendet werden
(vgl. Folie 222):
Wenn das Muster auf den Wert des Ausdrucks passt und σ die
zugehörige Substitution ist, werden die Bezeichner im Muster gemäß
σ an die zugehörige Werte gebunden.
<Funktionsbez > <Parametermuster > ... =
...
<Funktionsbez > <Parametermuster > ... =
<Ausdruck >
<Ausdruck >
Wenn das Muster auf den Wert des Ausdrucks nich passt, wird eine
Ausnahme erzeugt, sobald auf einen der deklarierten Bezeichner
zugegriffen wird.
Bei der Funktionsanwendung wird der Reihe nach geprüft, auf welches
Parametermuster der aktuelle Parameter passt (vgl. Folie 229).
Beispiel:
Die Gleichung zum ersten passenden Fall wird verwendet.
(x, y)
=
©Arnd Poetzsch-Heffter
(4, 5);
TU Kaiserslautern
3. Funktionales Programmieren
257
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Funktionsdeklaration mit Mustern)
258
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Funktionsdeklaration mit Mustern) (2)
2. Deklaration von ist_sortiert::[Int] ->Bool mit drei Mustern:
ist_sortiert []
= True
ist_sortiert (x:[])
= True
ist_sortiert (x1:x2:xs) = if x1 <= x2
then ist_sortiert (x2:xs
)
else False
1. Deklaration von foldplus ohne Muster:
foldplus :: [Int] -> Int
foldplus xl = if null xl then 0
else (head xl) + foldplus (tail xl)
Deklaration von foldplus mit Muster:
Deklaration mit drei Mustern und Wächtern:
foldplus :: [Int] -> Int
foldplus []
= 0
foldplus (x:xl) = x + foldplus xl
©Arnd Poetzsch-Heffter
TU Kaiserslautern
ist_sortiert []
ist_sortiert (x:[])
ist_sortiert (x1:x2:xs)
| x1 <= x2
| otherwise
259
©Arnd Poetzsch-Heffter
=
=
True
True
=
=
ist_sortiert (x2:xs)
False
TU Kaiserslautern
260
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiele: (Funktionsdeklaration mit Mustern) (3)
Beispiele: (Funktionsdeklaration mit Mustern) (4)
Deklaration von ist_sortiert::[Int]->Bool mit zwei Mustern und
Wächtern:
ist_sortiert (x1:x2:xs)
| x1 <= x2
=
| otherwise
=
ist_sortiert x
=
3.1 Grundkonzepte funktionaler Programmierung
4. Verwendung geschachtelter Muster:
unzip :: [(a, b)] -> ([a],[b])
unzip []
= ([], [])
unzip ((x,y):ps) = ( (x : (fst (unzip ps))),
(y : (snd (unzip ps))) )
ist_sortiert (x2:xs)
False
True
3. Deklaration von append ::[a]->[a]->[a]:
append ([] , l2)
= l2
append ((x:xs), l2) = x : ( append (xs , l2))
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
261
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
let-Ausdruck
Beispiele: (let-Ausdruck)
Der Mustermechanismus kann auch innerhalb von Ausdrücken
eingesetzt werden.
a = let a = 2*3
in a*a
Syntax des let-Ausdrucks:
b = let a = 2*3
in let (b,c) = (a,a+1)
in a*b*c
let <Liste von Deklarationen >
in <Ausdruck >
TU Kaiserslautern
3.1 Grundkonzepte funktionaler Programmierung
unzip :: [(a, b)] -> ([a], [b])
unzip []
= ([] , [])
unzip ((x,y):ps) = let (xs , ys) = unzip ps
in ((x:xs), (y:ys))
Die aus den Deklarationen resultierenden Bindungen sind nur im
let-Ausdruck gültig. D.h. sie sind sichtbar im let-Ausdruck an den
Stellen, an denen sie nicht verdeckt sind.
©Arnd Poetzsch-Heffter
262
263
©Arnd Poetzsch-Heffter
TU Kaiserslautern
264
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
case-Ausdruck
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (case-Ausdruck)
Syntax des case-Ausdrucks:
case <Ausdruck0 >
of
<Muster1 > -> <Ausdruck1 >
...
<MusterN > -> <AusdruckN >
ist_sortiert xl =
case xl of
[]
-> True
(x:[])
-> True
(x1:x2:xs) -> if x1 <= x2
then ist_sortiert (x2:xs)
else False
Prüfe der Reihe nach, ob der resultierende Wert von <Ausdruck0> auf
eines der Muster passt.
Passt er auf ein Muster, nehme die entsprechenden Bindungen vor
und werte den zugehörigen Ausdruck aus (die Bindungen sind nur in
dem zugehörigen Ausdruck gültig).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
265
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen:
266
3.1 Grundkonzepte funktionaler Programmierung
Unterabschnitt 3.1.4
• Das Verbot von gleichen Bezeichnern in Mustern hat im
Wesentlichen den Grund, dass nicht für alle Werte/Typen die
Gleichheitsoperation definiert ist.
mal2
=
twotimes =
(a,a)
=
Benutzerdefinierte Datentypen
\x -> 2*x
\x -> x+x
(mal2 , twotimes )
• Wenn keines der angegebenen Muster passt, wird eine
Ausnahme erzeugt (abrupte Terminierung).
©Arnd Poetzsch-Heffter
TU Kaiserslautern
267
©Arnd Poetzsch-Heffter
TU Kaiserslautern
268
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Benutzerdefinierte Datentypen
3.1 Grundkonzepte funktionaler Programmierung
Vereinbarung von Typbezeichnern
Haskell erlaubt es, Bezeichner für Typen zu deklarieren (vgl. F. 209):
Fast alle modernen Spezifikations- und Programmiersprachen
gestatten es dem Benutzer, „neue“ Typen zu definieren.
type IntPaar
= (Int ,Int) ;
type CharList
= [Char] ;
type Telefonbuch =
[(( String ,String ,String ,Int) ,[ String ])] ;
Übersicht:
• Vereinbarung von Typbezeichnern
type IntegerNachInteger
• Deklaration neuer Typen
=
Integer -> Integer ;
fakultaet :: IntegerNachInteger ;
-- Argument muss >= 0 sein
fakultaet = fac ;
• Summentypen
• Rekursive Datentypen
Dabei wird kein neuer Typ definiert, sondern nur ein “neuer”
Bezeichner an einen bekannten Typ gebunden.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
269
3.1 Grundkonzepte funktionaler Programmierung
3.1 Grundkonzepte funktionaler Programmierung
Neue Typen werden in Haskell mit der datatype-Deklaration definiert,
die im Folgenden schrittweise erläutert wird.
Verdeutlichung benutzt werden (siehe Typ Telefonbuch).
Definition eines neuen Typs und Konstruktors:
• Zwei unterschiedliche Bezeichner können den gleichen Typ
data <NeuerTyp > =
bezeichnen; z.B.:
<Konstruktor >
<Typ1 > ... <TypN >
Die obige Datatypdeklaration definiert:
(Int ,Int ,Int)
(Int ,Int ,Int)
• einen neuen Typ und bindet ihn an <NeuerTyp>
• eine Konstruktorfunktion mit Signatur
kalenderwoche :: Date -> Int
-- Parameter muss existierenden Kalendertag sein
kalenderwoche (tag ,monat ,jahr) = ...
<Konstruktor >:: <Typ1 > -> ... -> <TypN > -> <NeuerTyp >
Die Konstruktorfunktion ist injektiv.
kalenderwoche (11 ,12 ,2003)
TU Kaiserslautern
270
Deklaration neuer Typen
• Typvereinbarungen können zur Abkürzung oder zur
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
Bemerkungen: (Typvereinbarungen)
type IntTriple =
type Date
=
©Arnd Poetzsch-Heffter
Typ- und Konstruktorbezeichner müssen mit einem Großbuchstaben
beginnen.
271
©Arnd Poetzsch-Heffter
TU Kaiserslautern
272
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Beispiel: (Definition von Typ, Konstruktor, Selektoren)
3.1 Grundkonzepte funktionaler Programmierung
Bemerkungen:
data Person = Student String String Int Int
definiert den neuen Typ Person und den Konstruktor
Student :: String -> String -> Int -> Int -> Person
Jede Datentypdeklaration definiert einen neuen Typ, d.h.
insbesondere:
Wir definieren dazu folgende Selektorfunktionen:
vorname :: Person -> String
vorname ( Student v n g m)
• die Werte des neuen Typs sind inkompatibel mit allen anderen
= v
Typen;
• auch Werte strukturgleicher benutzerdefinierter Typen sind
name :: Person -> String
name ( Student v n g m)
inkompatibel.
= n
geburtsdatum :: Person -> Int
geburtsdatum ( Student v n g m) = g
matriknr :: Person -> Int
matriknr ( Student v n g m)
©Arnd Poetzsch-Heffter
= m
TU Kaiserslautern
3. Funktionales Programmieren
273
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Typkompatibilität)
274
3.1 Grundkonzepte funktionaler Programmierung
Bemerkung:
1. Der Typ Person ist inkompatibel mit dem Tupeltyp
type Person2 =
(String ,String ,Int ,Int)
Den Konstruktor kann man sich als eine Markierung der Werte seines
Argumentbereichs vorstellen.
Insbesondere ist vorname ("Niels","Bohr",18851007,221) nicht
typkorrekt.
Dabei werden Werte mit unterschiedlicher Markierung als verschieden
betrachtet.
2. Person ist inkompatibel mit dem strukturgleichen Typ Adresse:
data Adresse = Wohnung String String Int Int
Konstruktoren erlauben es in gewisser Weise neue Produkttypen zu
definieren.
Insbesondere ist
name ( Wohnung " Casimirring " " Lautern " 27 67663 )
nicht typkorrekt.
©Arnd Poetzsch-Heffter
TU Kaiserslautern
275
©Arnd Poetzsch-Heffter
TU Kaiserslautern
276
3. Funktionales Programmieren
3.1 Grundkonzepte funktionaler Programmierung
3. Funktionales Programmieren
Summentypen
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Summentypen)
Ein Summentyp stellt die disjunkte Vereinigung der Elemente anderen
Typen zu einem neuen Typ dar.
Die meisten modernen Programmiersprachen unterstützen die
Deklaration von Summentypen.
1. Ein anderer Datentyp zur Behandlung von Personen:
In Haskell definiert man Summentypen durch Angabe von Alternativen
bei der datatype-Deklaration:
data <NeuerTyp > =
<Konstruktor1 >
|
<Konstruktor2 >
...
|
<KonstruktorM >
data Person2 =
Student
String String Int Int
| Mitarbeiter String String Int Int
| Professor
String String Int Int String
<Typ1_1 > ... <Typ1_N1 >
<Typ2_1 > ... <Typ2_N2 >
<TypM_1 > ... <TypM_NM >
©Arnd Poetzsch-Heffter
TU Kaiserslautern
3. Funktionales Programmieren
277
©Arnd Poetzsch-Heffter
3.1 Grundkonzepte funktionaler Programmierung
TU Kaiserslautern
3. Funktionales Programmieren
Beispiele: (Summentypen) (2)
278
3.1 Grundkonzepte funktionaler Programmierung
Beispiele: (Summentypen) (3)
2. Eine benutzerdefinierte Datenstruktur für Zahlen:
data MyNumber = Intc
| Floatc
isInt (Intc m)
isInt ( Floatc r)
isFloat (Intc m)
isFloat ( Floatc r)
=
=
=
=
Int
Float
plus :: MyNumber -> MyNumber -> MyNumber
plus (Intc m) (Intc n)
= Intc (m+n)
plus (Intc m) ( Floatc r)
=
Floatc (( fromInteger ( toInteger m))+r)
plus ( Floatc r) (Intc m)
=
Floatc (r+( fromInteger ( toInteger m)))
plus ( Floatc r) ( Floatc q) = Floatc (r+q)
True
False
False
True
neg :: MyNumber -> MyNumber
neg (Intc m)
= Intc (-m)
neg ( Floatc r) = Floatc (-r)
©Arnd Poetzsch-Heffter
TU Kaiserslautern
279
©Arnd Poetzsch-Heffter
TU Kaiserslautern
280
Herunterladen