Meine Frage bezieht sich auf eine allgemeinere Frage zum Haskell-Programmdesign. Ich möchte mich jedoch auf einen bestimmten Anwendungsfall konzentrieren.
Ich habe einen Datentyp (z. B. Foo
) definiert und ihn in einer Funktion (z. B. f
) durch Mustervergleich verwendet. Später erkannte ich, dass der Typ ( Foo
) ein zusätzliches Feld benötigt, um neue Funktionalitäten zu unterstützen. Das Hinzufügen des Felds würde jedoch ändern, wie der Typ verwendet werden kann; d.h. die existierenden Funktionen, die von dem Typ abhängig sind, könnten betroffen sein. Das Hinzufügen neuer Funktionalitäten zu bestehendem Code, wie unattraktiv sie auch sein mag, ist schwer zu vermeiden. Ich frage mich, welche Best Practices auf der Haskell-Sprache die Auswirkungen solcher Änderungen minimieren.
Der vorhandene Code lautet beispielsweise:
%Vor% Die Funktion f
wird falsch geschrieben, wenn ich ein weiteres Feld zu Foo
:
Allerdings, wenn ich die Funktion f
an erster Stelle wie folgt definiert hätte:
, dann wäre selbst mit der Änderung an Foo
, f
immer noch korrekt.
Linsen lösen dieses Problem gut. Definieren Sie einfach ein Objektiv, das auf das interessierende Feld zeigt:
%Vor%Sie können dieses Objektiv als Getter verwenden:
%Vor%... ein Setter:
%Vor%... und ein Mapper:
%Vor% Der beste Teil ist, dass Sie, wenn Sie später die interne Darstellung Ihres Datentyps ändern, lediglich die Implementierung von v
ändern müssen, um auf die neue Position des Interessensbereichs zu zeigen. Wenn deine nachgeschalteten Benutzer nur das Objektiv verwenden, um mit deinem Foo
zu interagieren, wirst du die Rückwärtskompatibilität nicht durchbrechen.
Die beste Vorgehensweise für Verarbeitungstypen, die möglicherweise neue Felder hinzufügen, die Sie im vorhandenen Code ignorieren möchten, ist in der Tat die Verwendung von Datensatzselektoren, wie Sie es getan haben.
Ich würde sagen, dass Sie immer einen Typ definieren sollten, der sich unter Verwendung der Datensatznotation ändern könnte, und Sie sollten niemals einen Mustervergleich mit einem mit der Datensatznotation definierten Typ unter Verwendung des ersten Stils mit Positionsargumenten durchführen.
Eine andere Möglichkeit, den obigen Code auszudrücken, ist:
%Vor% Dies ist wohl eleganter, und es funktioniert auch besser in dem Fall, in dem Foo
mehrere Datenkonstruktoren hat.
Ihre f
-Funktion ist so einfach, dass die einfachste Antwort darin wäre, sie in Punkt-freiem Stil mit composition:
Wenn Ihre Funktion mehr als ein Feld vom Wert Foo
benötigt, funktioniert das obige nicht. Aber wir könnten die Applicative
-Instanz für (->)
verwenden und den folgenden Trick ausführen:
Bei Funktionen wendet liftA2
ein Eingabeargument auf zwei Funktionen an und kombiniert dann die Ergebnisse in einer anderen Funktion, (++)
in diesem Fall. Aber vielleicht grenzt das an das Dunkle.