Es gibt einige Probleme, die von algebraischen Datentypen leicht gelöst werden können. Beispielsweise kann ein Listentyp sehr prägnant ausgedrückt werden als:
data ConsList a = Empty | ConsCell a (ConsList a)
consmap f Empty = Empty
consmap f (ConsCell a b) = ConsCell (f a) (consmap f b)
l = ConsCell 1 (ConsCell 2 (ConsCell 3 Empty))
consmap (+1) l
Dieses spezielle Beispiel ist in Haskell enthalten, es ist jedoch in anderen Sprachen ähnlich, da die Unterstützung für algebraische Datentypen nativ ist.
Es stellt sich heraus, dass es eine offensichtliche Zuordnung zur Subtypisierung im OO-Stil gibt: Der Datentyp wird zu einer abstrakten Basisklasse und jeder Datenkonstruktor wird zu einer konkreten Subklasse. Hier ist ein Beispiel in Scala:
sealed abstract class ConsList[+T] {
def map[U](f: T => U): ConsList[U]
}
object Empty extends ConsList[Nothing] {
override def map[U](f: Nothing => U) = this
}
final class ConsCell[T](first: T, rest: ConsList[T]) extends ConsList[T] {
override def map[U](f: T => U) = new ConsCell(f(first), rest.map(f))
}
val l = (new ConsCell(1, new ConsCell(2, new ConsCell(3, Empty)))
l.map(1+)
Das einzige, was über die naive Unterklasse hinaus benötigt wird, ist eine Methode zum Versiegeln von Klassen, dh eine Methode, die das Hinzufügen von Unterklassen zu einer Hierarchie unmöglich macht.
Wie würden Sie dieses Problem in einer Sprache wie C # oder Java angehen? Die beiden Stolpersteine, die ich bei der Verwendung algebraischer Datentypen in C # gefunden habe, waren:
- Ich konnte nicht herausfinden, wie der unterste Typ in C # heißt (dh ich konnte nicht herausfinden, in was ich ihn einfügen soll
class Empty : ConsList< ??? >
) - Ich konnte keine Methode zum Versiegeln finden,
ConsList
sodass der Hierarchie keine Unterklassen hinzugefügt werden können
Was wäre der idiomatischste Weg, um algebraische Datentypen in C # und / oder Java zu implementieren? Oder, wenn es nicht möglich ist, was wäre der idiomatische Ersatz?
Antworten:
Es gibt eine einfache, aber sehr umfangreiche Möglichkeit, Klassen in Java zu versiegeln. Sie fügen einen privaten Konstruktor in die Basisklasse ein und erstellen daraus Unterklassen innerer Klassen.
Heften Sie ein Besuchermuster für den Versand an.
Mein Projekt jADT: Java Algebraic DataTypes generiert das gesamte Boilerplate für Sie https://github.com/JamesIry/jADT
quelle
Either
. Siehe meine FrageSie können dies erreichen, indem Sie das Besuchermuster verwenden , das den Mustervergleich ergänzt. Zum Beispiel
kann in Java als geschrieben werden
Die Versiegelung erfolgt durch die
Visitor
Klasse. Jede ihrer Methoden deklariert, wie eine der Unterklassen dekonstruiert wird. Sie könnten weitere Unterklassen hinzufügen, diese müssten jedoch implementiertaccept
und durch Aufrufen einer dervisit...
Methoden aufgerufen werden, sodass sie sich entweder wieCons
oder wie verhalten müsstenNil
.quelle
Wenn Sie benannte C # -Parameter missbrauchen (eingeführt in C # 4.0), können Sie algebraische Datentypen erstellen, mit denen Sie leicht übereinstimmen können:
Hier ist die Implementierung der
Either
Klasse:quelle
class Right<R> : Either<Bot,R>
:, wobei Entweder in eine Schnittstelle mit kovarianten (out) Typparametern geändert wird und Bot der unterste Typ ist (Subtyp jedes anderen Typs, gegenüber von Object). Ich glaube nicht, dass C # einen Bottom-Typ hat.In C # können Sie diesen
Empty
Typ nicht haben , da die Basistypen aufgrund der Reifizierung für verschiedene Elementtypen unterschiedlich sind. Sie können nur habenEmpty<T>
; nicht so nützlich.In Java kann es
Empty : ConsList
sein, dass der Typ gelöscht wird, aber ich bin mir nicht sicher, ob die Typprüfung nicht irgendwo schreien würde.Da jedoch beide Sprachen vorhanden sind
null
, können Sie sich alle Referenztypen als "Whatever | Null" vorstellen. Verwenden Sie also einfach dennull
als "Leer", um zu vermeiden, dass Sie angeben müssen, was er ableitet.quelle
null
ist, dass es zu allgemein ist: Es repräsentiert das Fehlen von irgendetwas , dh die Leere im Allgemeinen, aber ich möchte das Fehlen von Listenelementen darstellen, dh eine leere Liste im Besonderen. Eine leere Liste und ein leerer Baum sollten unterschiedliche Typen haben. Außerdem muss die leere Liste ein tatsächlicher Wert sein, da sie immer noch ein eigenes Verhalten aufweist und daher über eigene Methoden verfügen muss. Um die Liste[1, 2, 3]
zu erstellen, möchte ich sagenEmpty.prepend(3).prepend(2).prepend(1)
(oder in einer Sprache mit rechtsassoziativen Operatoren1 :: 2 :: 3 :: Empty
), aber ich kann nicht sagennull.prepend …
.Empty
undEmpty<>
und missbrauchen, um eine recht praktische Simulation zu ermöglichen. Im Wesentlichen verwenden SieEmpty
in Code, aber alle Typensignaturen usw. verwenden nur die generischen Varianten.In Java können Sie nicht. Sie können die Basisklasse jedoch als Paket privat deklarieren. Dies bedeutet, dass alle direkten Unterklassen zu demselben Paket gehören müssen wie die Basisklasse. Wenn Sie dann die Unterklassen als final deklarieren, können sie nicht weiter untergeordnet werden.
Ich weiß nicht, ob dies Ihr eigentliches Problem angehen würde ...
quelle
intanceof
Checks "pseudotypsicher" machen (dh: Ich weiß, dass es sicher ist, auch wenn der Compiler dies nicht tut), indem ich einfach immer sicherstelle Überprüfen Sie diese beiden Fälle. Wenn jedoch eine andere Person eine neue Unterklasse hinzufügt, kann es zu Laufzeitfehlern kommen, mit denen ich nicht gerechnet habe.Der Datentyp
ConsList<A>
kann als Schnittstelle dargestellt werden. Die Schnittstelle stellt eine einzigedeconstruct
Methode zur Verfügung, mit der Sie einen Wert dieses Typs "dekonstruieren" können, dh jeden der möglichen Konstruktoren handhaben können. Aufrufe einerdeconstruct
Methode entsprechen einemcase of
Formular in Haskell oder ML.Die
deconstruct
Methode verwendet eine "Rückruffunktion" für jeden Konstruktor im ADT. In unserem Fall wird eine Funktion für den leeren Listenfall und eine weitere Funktion für den Fall "cons cell" verwendet.Jede Rückruffunktion akzeptiert als Argumente die Werte, die vom Konstruktor akzeptiert werden. Für den Fall "leere Liste" sind also keine Argumente zulässig, für den Fall "cons cell" sind jedoch zwei Argumente zulässig: der Kopf und der Schwanz der Liste.
Wir können diese "Mehrfachargumente" mit
Tuple
Klassen oder mit Currying codieren . In diesem Beispiel habe ich mich für eine einfachePair
Klasse entschieden.Die Schnittstelle wird für jeden Konstruktor einmal implementiert. Zunächst haben wir die Implementierung für die "leere Liste". Die
deconstruct
Implementierung ruft einfach dieemptyCase
Rückruffunktion auf.Dann implementieren wir den Fall "cons cell" auf ähnliche Weise. Diesmal hat die Klasse Eigenschaften: Kopf und Ende der nicht leeren Liste. In der
deconstruct
Implementierung werden diese Eigenschaften an dieconsCase
Rückruffunktion übergeben.Hier ist ein Beispiel für die Verwendung dieser Codierung von ADTs: Wir können eine
reduce
Funktion schreiben , bei der es sich um die üblichen Fold-Over-Listen handelt.Dies ist analog zu dieser Implementierung in Haskell:
quelle
Es gibt keine gute Möglichkeit, dies zu tun, aber wenn Sie bereit sind, mit einem abscheulichen Hack zu leben, können Sie dem Konstruktor der abstrakten Basisklasse eine explizite Typprüfung hinzufügen. In Java wäre dies so etwas wie
In C # ist es aufgrund der überarbeiteten Generika komplizierter - der einfachste Ansatz könnte darin bestehen, den Typ in eine Zeichenfolge zu konvertieren, die sich läst.
Beachten Sie, dass in Java sogar dieser Mechanismus theoretisch von jemandem umgangen werden kann, der über das Serialisierungsmodell oder wirklich möchte
sun.misc.Unsafe
.quelle
Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();