Wenn Sie etwas genauer hinschauen, enthalten beide auch Arrays in der Basissprache:
- Der 5. überarbeitete Schema Report (R5RS) den Vektor - Typ , die mit fester Größe integer-indizierte Sammlungen mit besser als lineare Zeit für den Direktzugriff.
- Der Haskell 98-Bericht hat auch einen Array-Typ .
Die funktionale Programmieranweisung hat jedoch lange Zeit einfach verknüpfte Listen gegenüber Arrays oder doppelt verknüpften Listen hervorgehoben. Wahrscheinlich sogar überbetont. Es gibt jedoch mehrere Gründe dafür.
Erstens sind einfach verknüpfte Listen einer der einfachsten und dennoch nützlichsten rekursiven Datentypen. Ein benutzerdefiniertes Äquivalent zum Listentyp von Haskell kann folgendermaßen definiert werden:
data List a -- A list with element type `a`...
= Empty -- is either the empty list...
| Cell a (List a) -- or a pair with an `a` and the rest of the list.
Die Tatsache, dass Listen ein rekursiver Datentyp sind, bedeutet, dass die Funktionen, die mit Listen arbeiten, im Allgemeinen eine strukturelle Rekursion verwenden . In Haskell Bedingungen: Sie Mustererkennung auf der Liste Bauer, und Sie Rekursion auf einem subpart der Liste. In diesen beiden grundlegenden Funktionsdefinitionen verwende ich die Variable as
, um auf das Ende der Liste zu verweisen. Beachten Sie also, dass die rekursiven Aufrufe in der Liste "absteigen":
map :: (a -> b) -> List a -> List b
map f Empty = Empty
map f (Cell a as) = Cell (f a) (map f as)
filter :: (a -> Bool) -> List a -> List a
filter p Empty = Empty
filter p (Cell a as)
| p a = Cell a (filter p as)
| otherwise = filter p as
Diese Technik garantiert, dass Ihre Funktion für alle endlichen Listen beendet wird, und ist auch eine gute Technik zur Problemlösung - sie teilt Probleme auf natürliche Weise in einfachere, haltbarere Unterabschnitte auf.
Einfach verknüpfte Listen sind daher wahrscheinlich der beste Datentyp, um die Schüler in diese Techniken einzuführen, die für die funktionale Programmierung sehr wichtig sind.
Der zweite Grund ist weniger ein Grund für "Warum einfach verknüpfte Listen" als vielmehr ein Grund für "Warum nicht doppelt verknüpfte Listen oder Arrays": Diese letzteren Datentypen erfordern häufig eine Mutation (modifizierbare Variablen), die sehr häufig funktioniert scheut sich vor. So wie es passiert:
- In einer eifrigen Sprache wie Scheme können Sie keine doppelt verknüpfte Liste erstellen, ohne Mutation zu verwenden.
- In einer faulen Sprache wie Haskell können Sie eine doppelt verknüpfte Liste erstellen, ohne eine Mutation zu verwenden. Wenn Sie jedoch eine neue Liste erstellen, die auf dieser Liste basiert, müssen Sie die meisten, wenn nicht die gesamte Struktur des Originals kopieren. Während Sie mit einfach verknüpften Listen Funktionen schreiben können, die "Strukturfreigabe" verwenden, können neue Listen die Zellen alter Listen bei Bedarf wiederverwenden.
- Wenn Sie Arrays unveränderlich verwendet haben, bedeutete dies traditionell, dass Sie jedes Mal, wenn Sie das Array ändern wollten, das Ganze kopieren mussten. (Neuere Haskell-Bibliotheken wie
vector
haben jedoch Techniken gefunden, die dieses Problem erheblich verbessern).
Der dritte und letzte Grund gilt in erster Linie für faule Sprachen wie Haskell: Faule, einfach verknüpfte Listen ähneln in der Praxis häufig eher Iteratoren als eigentlichen In-Memory-Listen. Wenn Ihr Code die Elemente einer Liste nacheinander verbraucht und sie unterwegs auswirft, materialisiert der Objektcode nur die Listenzellen und ihren Inhalt, wenn Sie die Liste durchgehen.
Dies bedeutet, dass nicht die gesamte Liste gleichzeitig im Speicher vorhanden sein muss, sondern nur die aktuelle Zelle. Zellen vor der aktuellen können durch Müll gesammelt werden (was mit einer doppelt verknüpften Liste nicht möglich wäre). Zellen, die später als die aktuelle sind, müssen erst berechnet werden, wenn Sie dort ankommen.
Es geht noch weiter. In mehreren gängigen Haskell-Bibliotheken, der so genannten Fusion , wird eine Technik verwendet , bei der der Compiler Ihren Listenverarbeitungscode analysiert und Zwischenlisten erkennt, die nacheinander generiert und konsumiert und dann "weggeworfen" werden. Mit diesem Wissen kann der Compiler dann die Speicherzuordnung der Zellen dieser Listen vollständig eliminieren. Dies bedeutet, dass eine einfach verknüpfte Liste in einem Haskell-Quellprogramm nach der Kompilierung möglicherweise tatsächlich in eine Schleife anstelle einer Datenstruktur umgewandelt wird.
Fusion ist auch die Technik, mit der die oben genannte vector
Bibliothek effizienten Code für unveränderliche Arrays generiert. Gleiches gilt für die äußerst beliebten Bibliotheken bytestring
(Byte-Arrays) und text
(Unicode-Strings), die als Ersatz für Haskells nicht sehr guten nativen String
Typ (der [Char]
mit einer einfach verknüpften Liste von Zeichen identisch ist ) erstellt wurden. Im modernen Haskell gibt es also einen Trend, bei dem unveränderliche Array-Typen mit Fusionsunterstützung sehr verbreitet sind.
Die Listenfusion wird durch die Tatsache erleichtert, dass Sie in einer einfach verknüpften Liste vorwärts, aber niemals rückwärts gehen können . Dies wirft ein sehr wichtiges Thema in der funktionalen Programmierung auf: Verwenden der "Form" eines Datentyps, um die "Form" einer Berechnung abzuleiten. Wenn Sie Elemente nacheinander verarbeiten möchten, ist eine einfach verknüpfte Liste ein Datentyp, der Ihnen bei Verwendung mit struktureller Rekursion dieses Zugriffsmuster auf ganz natürliche Weise bietet. Wenn Sie eine "Divide and Conquer" -Strategie verwenden möchten, um ein Problem anzugreifen, unterstützen Baumdatenstrukturen dies in der Regel sehr gut.
Viele Leute verlassen den funktionalen Programmierwagen frühzeitig, um sich mit den einfach verknüpften Listen vertraut zu machen, aber nicht mit den fortgeschritteneren zugrunde liegenden Ideen.