Was ist "Pattern Matching" in funktionalen Sprachen?


Antworten:


141

Um den Mustervergleich zu verstehen, müssen drei Teile erklärt werden:

  1. Algebraische Datentypen.
  2. Was ist Mustervergleich?
  3. Warum ist es so toll?

Algebraische Datentypen auf den Punkt gebracht

Mit ML-ähnlichen Funktionssprachen können Sie einfache Datentypen definieren, die als "disjunkte Vereinigungen" oder "algebraische Datentypen" bezeichnet werden. Diese Datenstrukturen sind einfache Container und können rekursiv definiert werden. Beispielsweise:

type 'a list =
    | Nil
    | Cons of 'a * 'a list

definiert eine stapelartige Datenstruktur. Betrachten Sie es als äquivalent zu diesem C #:

public abstract class List<T>
{
    public class Nil : List<T> { }
    public class Cons : List<T>
    {
        public readonly T Item1;
        public readonly List<T> Item2;
        public Cons(T item1, List<T> item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }
    }
}

Die Bezeichner Consund Nildefinieren einfach eine einfache Klasse, wobei die of x * y * z * ...einen Konstruktor und einige Datentypen definiert. Die Parameter für den Konstruktor sind unbenannt und werden durch Position und Datentyp identifiziert.

Sie erstellen Instanzen Ihrer a listKlasse als solche:

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))

Welches ist das gleiche wie:

Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));

Mustervergleich auf den Punkt gebracht

Pattern Matching ist eine Art Typprüfung. Nehmen wir also an, wir haben ein Stapelobjekt wie das oben beschriebene erstellt. Wir können Methoden implementieren, um den Stapel wie folgt zu betrachten und zu öffnen:

let peek s =
    match s with
    | Cons(hd, tl) -> hd
    | Nil -> failwith "Empty stack"

let pop s =
    match s with
    | Cons(hd, tl) -> tl
    | Nil -> failwith "Empty stack"

Die obigen Methoden entsprechen (obwohl nicht als solche implementiert) dem folgenden C #:

public static T Peek<T>(Stack<T> s)
{
    if (s is Stack<T>.Cons)
    {
        T hd = ((Stack<T>.Cons)s).Item1;
        Stack<T> tl = ((Stack<T>.Cons)s).Item2;
        return hd;
    }
    else if (s is Stack<T>.Nil)
        throw new Exception("Empty stack");
    else
        throw new MatchFailureException();
}

public static Stack<T> Pop<T>(Stack<T> s)
{
    if (s is Stack<T>.Cons)
    {
        T hd = ((Stack<T>.Cons)s).Item1;
        Stack<T> tl = ((Stack<T>.Cons)s).Item2;
        return tl;
    }
    else if (s is Stack<T>.Nil)
        throw new Exception("Empty stack");
    else
        throw new MatchFailureException();
}

(Fast immer implementieren ML-Sprachen Pattern Matching ohne Laufzeit-Typentests oder Casts, daher täuscht der C # -Code etwas. Lassen Sie uns die Implementierungsdetails mit ein paar Handbewegungen beiseite schieben :))

Datenstrukturzerlegung auf den Punkt gebracht

Ok, kehren wir zur Peek-Methode zurück:

let peek s =
    match s with
    | Cons(hd, tl) -> hd
    | Nil -> failwith "Empty stack"

Der Trick besteht darin zu verstehen, dass die Bezeichner hdund tlBezeichner Variablen sind (ähm ... da sie unveränderlich sind, sind sie nicht wirklich "Variablen", sondern "Werte";)). Wenn sder Typ vorhanden ist Cons, ziehen wir seine Werte aus dem Konstruktor heraus und binden sie an die Variablen mit dem Namen hdundtl .

Der Mustervergleich ist nützlich, da wir damit eine Datenstruktur anhand ihrer Form anstelle ihres Inhalts zerlegen können . Stellen Sie sich also vor, wir definieren einen Binärbaum wie folgt:

type 'a tree =
    | Node of 'a tree * 'a * 'a tree
    | Nil

Wir können einige Baumrotationen wie folgt definieren:

let rotateLeft = function
    | Node(a, p, Node(b, q, c)) -> Node(Node(a, p, b), q, c)
    | x -> x

let rotateRight = function
    | Node(Node(a, p, b), q, c) -> Node(a, p, Node(b, q, c))
    | x -> x

(Der let rotateRight = functionKonstruktor ist Syntaxzucker für let rotateRight s = match s with ....)

Wir können also nicht nur die Datenstruktur an Variablen binden, sondern auch einen Drilldown durchführen. Nehmen wir an, wir haben einen Knoten let x = Node(Nil, 1, Nil). Wenn wir aufrufen rotateLeft x, testen wir anhand xdes ersten Musters, das nicht übereinstimmt, weil das richtige Kind den Typ Nilanstelle von hat Node. Es wird zum nächsten Muster übergehen,x -> x das mit jeder Eingabe übereinstimmt und unverändert zurückgegeben wird.

Zum Vergleich würden wir die obigen Methoden in C # schreiben als:

public abstract class Tree<T>
{
    public abstract U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc);

    public class Nil : Tree<T>
    {
        public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
        {
            return nilFunc();
        }
    }

    public class Node : Tree<T>
    {
        readonly Tree<T> Left;
        readonly T Value;
        readonly Tree<T> Right;

        public Node(Tree<T> left, T value, Tree<T> right)
        {
            this.Left = left;
            this.Value = value;
            this.Right = right;
        }

        public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
        {
            return nodeFunc(Left, Value, Right);
        }
    }

    public static Tree<T> RotateLeft(Tree<T> t)
    {
        return t.Match(
            () => t,
            (l, x, r) => r.Match(
                () => t,
                (rl, rx, rr) => new Node(new Node(l, x, rl), rx, rr))));
    }

    public static Tree<T> RotateRight(Tree<T> t)
    {
        return t.Match(
            () => t,
            (l, x, r) => l.Match(
                () => t,
                (ll, lx, lr) => new Node(ll, lx, new Node(lr, x, r))));
    }
}

Für ernst.

Pattern Matching ist fantastisch

Sie können mithilfe des Besuchermusters etwas Ähnliches wie den Mustervergleich in C # implementieren , es ist jedoch bei weitem nicht so flexibel, da Sie komplexe Datenstrukturen nicht effektiv zerlegen können. Wenn Sie den Mustervergleich verwenden, teilt Ihnen der Compiler außerdem mit, ob Sie einen Fall ausgelassen haben . Wie großartig ist das?

Überlegen Sie, wie Sie ähnliche Funktionen in C # oder Sprachen ohne Mustervergleich implementieren würden. Überlegen Sie, wie Sie es ohne Test-Tests und Casts zur Laufzeit machen würden. Es ist sicherlich nicht schwer , nur umständlich und sperrig. Und der Compiler überprüft nicht, ob Sie jeden Fall abgedeckt haben.

Der Pattern Matching hilft Ihnen also, Datenstrukturen in einer sehr praktischen, kompakten Syntax zu zerlegen und zu navigieren. So kann der Compiler die Logik Ihres Codes zumindest ein wenig überprüfen . Es ist wirklich ist ein Killer - Feature.


+1, aber vergessen Sie nicht andere Sprachen mit Mustervergleich wie Mathematica.
Jon Harrop

1
"ähm ... da sie unveränderlich sind, sind sie nicht wirklich" Variablen ", sondern" Werte ";)" Sie sind Variablen; Es ist die veränderliche Sorte, die falsch beschriftet ist . Trotzdem hervorragende Antwort!
Doval

3
"Fast immer implementieren ML-Sprachen Pattern Matching ohne Laufzeit-Typprüfungen oder Casts" <- Wie funktioniert das? Können Sie mich auf eine Grundierung hinweisen?
David Moles

1
@DavidMoles: Das Typsystem ermöglicht es, alle Laufzeitprüfungen zu umgehen, indem Musterübereinstimmungen als vollständig und nicht redundant nachgewiesen werden. Wenn Sie versuchen, einer Sprache wie SML, OCaml oder F # eine Musterübereinstimmung zu geben, die nicht vollständig ist oder Redundanz enthält, werden Sie vom Compiler beim Kompilieren gewarnt. Dies ist eine äußerst leistungsstarke Funktion, da Sie Laufzeitprüfungen vermeiden können, indem Sie Ihren Code neu anordnen, dh Sie können Aspekte Ihres Codes als korrekt erweisen. Darüber hinaus ist es leicht zu verstehen!
Jon Harrop

@ JonHarrop Ich kann sehen, wie das funktionieren würde (effektiv ähnelt es dem dynamischen Nachrichtenversand), aber ich kann nicht sehen, wie Sie zur Laufzeit einen Zweig ohne Typprüfung auswählen.
David Moles

33

Kurze Antwort: Mustervergleich entsteht, weil funktionale Sprachen das Gleichheitszeichen als Behauptung der Äquivalenz statt als Zuweisung behandeln.

Lange Antwort: Der Mustervergleich ist eine Form des Versands, die auf der „Form“ des angegebenen Werts basiert. In einer funktionalen Sprache sind die von Ihnen definierten Datentypen normalerweise sogenannte diskriminierte Gewerkschaften oder algebraische Datentypen. Was ist zum Beispiel eine (verknüpfte) Liste? Eine verknüpfte Liste Listvon Dingen eines Typs aist entweder die leere Liste Niloder ein Element des Typs, das a Consauf a List a(eine Liste von as) geschrieben ist. In Haskell (der funktionalen Sprache, mit der ich am besten vertraut bin) schreiben wir dies

data List a = Nil
            | Cons a (List a)

Alle diskriminierten Gewerkschaften werden folgendermaßen definiert: Ein einzelner Typ hat eine feste Anzahl verschiedener Möglichkeiten, ihn zu schaffen; Die Schöpfer werden wie Nilund Conshier Konstruktoren genannt. Dies bedeutet, dass ein Wert des Typs List amit zwei verschiedenen Konstruktoren erstellt werden konnte - er kann zwei verschiedene Formen haben. Angenommen, wir möchten eine headFunktion schreiben , um das erste Element der Liste zu erhalten. In Haskell würden wir dies als schreiben

-- `head` is a function from a `List a` to an `a`.
head :: List a -> a
-- An empty list has no first item, so we raise an error.
head Nil        = error "empty list"
-- If we are given a `Cons`, we only want the first part; that's the list's head.
head (Cons h _) = h

Da List aes zwei verschiedene Arten von Werten geben kann, müssen wir jeden einzeln behandeln. Dies ist der Mustervergleich. In head x, wenn xStreichhölzer das Muster Nil, dann führen wir den ersten Fall; Wenn es dem Muster entspricht Cons h _, führen wir das zweite aus.

Kurze Antwort, erklärt: Ich denke, eine der besten Möglichkeiten, über dieses Verhalten nachzudenken, besteht darin, Ihre Meinung zum Gleichheitszeichen zu ändern. In den Curly-Bracket-Sprachen bedeutet im Großen und Ganzen =Zuordnung: a = bbedeutet "machen ain b". In vielen funktionalen Sprachen bedeutet dies jedoch =eine Behauptung der Gleichheit: let Cons a (Cons b Nil) = frob x behauptet, dass das Ding auf der linken Seite Cons a (Cons b Nil)dem Ding auf der rechten Seite äquivalent ist frob x; Außerdem werden alle links verwendeten Variablen sichtbar. Dies passiert auch mit Funktionsargumenten: Wir behaupten, dass das erste Argument so aussieht Nil, und wenn dies nicht der Fall ist, überprüfen wir es weiter.


Was für eine interessante Art, über das Gleichheitszeichen nachzudenken. Danke, dass du das geteilt hast!
Jrahhali

2
Was heißt Consdas
Roymunson

2
@Roymunson: Consist die cons tructor , die eine (verknüpft) Liste aus einem Kopf aufbaut (das a) und ein Schwanz (der List a). Der Name kommt von Lisp. In Haskell ist es für den integrierten Listentyp der :Operator (der immer noch als "Nachteile" ausgesprochen wird).
Antal Spector-Zabusky

23

Es bedeutet, dass anstatt zu schreiben

double f(int x, int y) {
  if (y == 0) {
    if (x == 0)
      return NaN;
    else if (x > 0)
      return Infinity;
    else
      return -Infinity;
  } else
     return (double)x / y;
}

Du kannst schreiben

f(0, 0) = NaN;
f(x, 0) | x > 0 = Infinity;
        | else  = -Infinity;
f(x, y) = (double)x / y;

Hey, C ++ unterstützt auch Pattern Matching.

static const int PositiveInfinity = -1;
static const int NegativeInfinity = -2;
static const int NaN = -3;

template <int x, int y> struct Divide {
  enum { value = x / y };
};
template <bool x_gt_0> struct aux { enum { value = PositiveInfinity }; };
template <> struct aux<false> { enum { value = NegativeInfinity }; };
template <int x> struct Divide<x, 0> {
  enum { value = aux<(x>0)>::value };
};
template <> struct Divide<0, 0> {
  enum { value = NaN };
};

#include <cstdio>

int main () {
    printf("%d %d %d %d\n", Divide<7,2>::value, Divide<1,0>::value, Divide<0,0>::value, Divide<-1,0>::value);
    return 0;
};

1
In Scala: Importiere Double._ def divid = = Werte: (Double, Double) => Werte stimmen mit {case (0,0) => NaN case (x, 0) => if (x> 0) PositiveInfinity else NegativeInfinity case überein (x, y) => x / y}}
Fracca

12

Pattern Matching ähnelt einer überladenen Methode bei Steroiden. Der einfachste Fall wäre ungefähr der gleiche wie in Java. Argumente sind eine Liste von Typen mit Namen. Die richtige aufzurufende Methode basiert auf den übergebenen Argumenten und dient gleichzeitig als Zuordnung dieser Argumente zum Parameternamen.

Muster gehen nur einen Schritt weiter und können die übergebenen Argumente noch weiter zerstören. Es kann auch möglicherweise Wachen verwenden, um basierend auf dem Wert des Arguments tatsächlich eine Übereinstimmung zu erzielen. Zur Demonstration werde ich so tun, als hätte JavaScript einen Mustervergleich.

function foo(a,b,c){} //no pattern matching, just a list of arguments

function foo2([a],{prop1:d,prop2:e}, 35){} //invented pattern matching in JavaScript

In foo2 erwartet es, dass a ein Array ist, bricht das zweite Argument auseinander, erwartet ein Objekt mit zwei Requisiten (prop1, prop2) und weist den Variablen d und e die Werte dieser Eigenschaften zu und erwartet dann das dritte Argument 35.

Anders als in JavaScript erlauben Sprachen mit Mustervergleich normalerweise mehrere Funktionen mit demselben Namen, aber unterschiedlichen Mustern. Auf diese Weise ist es wie eine Methodenüberladung. Ich werde ein Beispiel in erlang geben:

fibo(0) -> 0 ;
fibo(1) -> 1 ;
fibo(N) when N > 0 -> fibo(N-1) + fibo(N-2) .

Verwischen Sie Ihre Augen ein wenig und Sie können sich dies in Javascript vorstellen. So etwas vielleicht:

function fibo(0){return 0;}
function fibo(1){return 1;}
function fibo(N) when N > 0 {return fibo(N-1) + fibo(N-2);}

Wenn Sie fibo aufrufen, basiert die verwendete Implementierung auf den Argumenten. Wenn Java jedoch auf Typen als einziges Mittel zum Überladen beschränkt ist, kann der Mustervergleich mehr bewirken.

Über die hier gezeigte Funktionsüberladung hinaus kann dasselbe Prinzip auch an anderen Stellen angewendet werden, z. B. bei Fallanweisungen oder bei der Destrukturierung von Assingments. JavaScript hat dies sogar in 1.7 .


8

Mit dem Mustervergleich können Sie einen Wert (oder ein Objekt) mit einigen Mustern abgleichen, um einen Zweig des Codes auszuwählen. Aus C ++ - Sicht klingt es möglicherweise etwas ähnlich wie die switchAnweisung. In funktionalen Sprachen kann der Mustervergleich zum Abgleichen von primitiven Standardwerten wie Ganzzahlen verwendet werden. Es ist jedoch nützlicher für zusammengesetzte Typen.

Lassen Sie uns zunächst den Mustervergleich für primitive Werte demonstrieren (unter Verwendung von erweitertem Pseudo-C ++ switch):

switch(num) {
  case 1: 
    // runs this when num == 1
  case n when n > 10: 
    // runs this when num > 10
  case _: 
    // runs this for all other cases (underscore means 'match all')
}

Die zweite Verwendung befasst sich mit funktionalen Datentypen wie Tupeln (mit denen Sie mehrere Objekte in einem einzigen Wert speichern können) und diskriminierten Vereinigungen, mit denen Sie einen Typ erstellen können, der eine von mehreren Optionen enthalten kann. Das klingt ein bisschen so, enumaußer dass jedes Etikett auch einige Werte enthalten kann. In einer Pseudo-C ++ - Syntax:

enum Shape { 
  Rectangle of { int left, int top, int width, int height }
  Circle of { int x, int y, int radius }
}

Ein Wert vom Typ Shapekann jetzt entweder Rectanglemit allen Koordinaten oder a Circlemit dem Mittelpunkt und dem Radius enthalten. Mit dem Mustervergleich können Sie eine Funktion für die Arbeit mit dem ShapeTyp schreiben :

switch(shape) { 
  case Rectangle(l, t, w, h): 
    // declares variables l, t, w, h and assigns properties
    // of the rectangle value to the new variables
  case Circle(x, y, r):
    // this branch is run for circles (properties are assigned to variables)
}

Schließlich können Sie auch verschachtelte Muster verwenden , die beide Funktionen kombinieren. Sie können beispielsweise verwenden Circle(0, 0, radius), um alle Formen abzugleichen, deren Mittelpunkt im Punkt [0, 0] liegt und die einen beliebigen Radius haben (der Wert des Radius wird der neuen Variablen zugewiesen radius).

Dies mag aus C ++ - Sicht etwas ungewohnt klingen, aber ich hoffe, dass mein Pseudo-C ++ die Erklärung klar macht. Die funktionale Programmierung basiert auf ganz anderen Konzepten, daher ist sie in einer funktionalen Sprache sinnvoller!


5

Beim Mustervergleich wählt der Interpreter für Ihre Sprache eine bestimmte Funktion basierend auf der Struktur und dem Inhalt der von Ihnen angegebenen Argumente aus.

Es ist nicht nur eine funktionale Sprachfunktion, sondern auch für viele verschiedene Sprachen verfügbar.

Das erste Mal, dass ich auf die Idee stieß, war, als ich Prolog lernte, wo es wirklich zentral für die Sprache ist.

z.B

last ([LastItem], LastItem).

last ([Head | Tail], LastItem): - last (Tail, LastItem).

Der obige Code gibt das letzte Element einer Liste an. Das Eingabearg ist das erste und das Ergebnis das zweite.

Wenn die Liste nur ein Element enthält, wählt der Interpreter die erste Version aus und das zweite Argument wird auf das erste gesetzt, dh dem Ergebnis wird ein Wert zugewiesen.

Wenn die Liste sowohl einen Kopf als auch einen Schwanz hat, wählt der Dolmetscher die zweite Version aus und wiederholt sie, bis nur noch ein Element in der Liste vorhanden ist.


Wie Sie dem Beispiel
entnehmen

4

Für viele Menschen ist es einfacher, ein neues Konzept zu erlernen, wenn einige einfache Beispiele angegeben werden.

Angenommen, Sie haben eine Liste mit drei Ganzzahlen und möchten das erste und das dritte Element hinzufügen. Ohne Mustervergleich könnten Sie dies folgendermaßen tun (Beispiele in Haskell):

Prelude> let is = [1,2,3]
Prelude> head is + is !! 2
4

Obwohl dies ein Spielzeugbeispiel ist, stellen Sie sich vor, wir möchten die erste und dritte Ganzzahl an Variablen binden und diese summieren:

addFirstAndThird is =
    let first = head is
        third = is !! 3
    in first + third

Diese Extraktion von Werten aus einer Datenstruktur ist das, was der Mustervergleich bewirkt. Sie "spiegeln" im Grunde die Struktur von etwas und geben Variablen an, die für die Orte von Interesse gebunden werden sollen:

addFirstAndThird [first,_,third] = first + third

Wenn Sie diese Funktion mit [1,2,3] als Argument aufrufen, wird [1,2,3] mit [first ,, _dritter] vereinheitlicht, wobei zuerst an 1, drittens an 3 gebunden und 2 verworfen wird ( _ist ein Platzhalter für Dinge, die dir egal sind).

Wenn Sie Listen nur mit 2 als zweitem Element abgleichen möchten, können Sie dies folgendermaßen tun:

addFirstAndThird [first,2,third] = first + third

Dies funktioniert nur für Listen mit 2 als zweitem Element und löst ansonsten eine Ausnahme aus, da für nicht übereinstimmende Listen keine Definition für addFirstAndThird angegeben ist.

Bisher haben wir Pattern Matching nur zur Destrukturierung der Bindung verwendet. Darüber hinaus können Sie mehrere Definitionen derselben Funktion angeben, wobei die erste Übereinstimmungsdefinition verwendet wird. Daher ähnelt der Mustervergleich ein wenig einer "switch-Anweisung für Stereoide":

addFirstAndThird [first,2,third] = first + third
addFirstAndThird _ = 0

addFirstAndThird fügt gerne das erste und dritte Element von Listen mit 2 als zweitem Element hinzu, andernfalls "fallen durch" und "geben" 0 zurück. Diese "switch-like" -Funktionalität kann nicht nur in Funktionsdefinitionen verwendet werden, z.

Prelude> case [1,3,3] of [a,2,c] -> a+c; _ -> 0
0
Prelude> case [1,2,3] of [a,2,c] -> a+c; _ -> 0
4

Darüber hinaus ist es nicht auf Listen beschränkt, sondern kann auch mit anderen Typen verwendet werden, z. B. mit den Wert- und Nichts-Wertkonstruktoren des Typs "Vielleicht", um den Wert zu "entpacken":

Prelude> case (Just 1) of (Just x) -> succ x; Nothing -> 0
2
Prelude> case Nothing of (Just x) -> succ x; Nothing -> 0
0

Sicher, das waren nur Spielzeugbeispiele, und ich habe nicht einmal versucht, eine formale oder erschöpfende Erklärung zu geben, aber sie sollten ausreichen, um das Grundkonzept zu verstehen.


3

Sie sollten mit der Wikipedia-Seite beginnen , die eine ziemlich gute Erklärung gibt. Lesen Sie dann das entsprechende Kapitel des Haskell-Wikibooks .

Dies ist eine schöne Definition aus dem obigen Wikibook:

Der Mustervergleich ist also eine Möglichkeit, Dingen Namen zuzuweisen (oder diese Namen an diese Dinge zu binden) und möglicherweise Ausdrücke gleichzeitig in Unterausdrücke zu zerlegen (wie wir es mit der Liste in der Definition der Karte getan haben).


3
Das nächste Mal werde ich in Frage erwähnen, dass ich bereits Wikipedia gelesen habe und es eine sehr schlechte Erklärung gibt.
Roman

2

Hier ist ein wirklich kurzes Beispiel, das die Nützlichkeit des Mustervergleichs zeigt:

Angenommen, Sie möchten ein Element in einer Liste sortieren:

["Venice","Paris","New York","Amsterdam"] 

zu (Ich habe "New York" sortiert)

["Venice","New York","Paris","Amsterdam"] 

in einer zwingenderen Sprache würden Sie schreiben:

function up(city, cities){  
    for(var i = 0; i < cities.length; i++){
        if(cities[i] === city && i > 0){
            var prev = cities[i-1];
            cities[i-1] = city;
            cities[i] = prev;
        }
    }
    return cities;
}

In einer funktionalen Sprache würden Sie stattdessen schreiben:

let up list value =  
    match list with
        | [] -> []
        | previous::current::tail when current = value ->  current::previous::tail
        | current::tail -> current::(up tail value)

Wie Sie sehen können, weist die musterangepasste Lösung weniger Rauschen auf. Sie können deutlich erkennen, welche Fälle es gibt und wie einfach es ist, unsere Liste zu bereisen und zu destrukturieren.

Ich habe eine detailliertere Blog - Post über sie geschrieben hier .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.