Gibt es eine Möglichkeit, einen Summentyp in Java zu definieren? Java scheint Produkttypen von Natur aus direkt zu unterstützen, und ich dachte, Aufzählungen könnten es ermöglichen, Summentypen zu unterstützen, und Vererbung sieht so aus, als könnte es das tun, aber es gibt mindestens einen Fall, den ich nicht lösen kann. Um es genauer auszuarbeiten, ein Summentyp ist ein Typ, der genau einen aus einer Reihe verschiedener Typen haben kann, wie eine getaggte Union in C. In meinem Fall versuche ich, haskells Entweder-Typ in Java zu implementieren:
data Either a b = Left a | Right b
aber auf der Basisebene muss ich es als Produkttyp implementieren und einfach eines seiner Felder ignorieren:
public class Either<L,R>
{
private L left = null;
private R right = null;
public static <L,R> Either<L,R> right(R right)
{
return new Either<>(null, right);
}
public static <L,R> Either<L,R> left(L left)
{
return new Either<>(left, null);
}
private Either(L left, R right) throws IllegalArgumentException
{
this.left = left;
this.right = right;
if (left != null && right != null)
{
throw new IllegalArgumentException("An Either cannot be created with two values");
}
if (left == right)
{
throw new IllegalArgumentException("An Either cannot be created without a value");
}
}
.
.
.
}
Ich habe versucht, dies mit Vererbung zu implementieren, aber ich muss einen Platzhaltertypparameter oder einen gleichwertigen Parameter verwenden, den Java-Generika nicht zulassen:
public class Left<L> extends Either<L,?>
Ich habe Javas Enums nicht viel verwendet, aber obwohl sie der nächstbeste Kandidat zu sein scheinen, bin ich nicht hoffnungsvoll.
An diesem Punkt denke ich, dass dies möglicherweise nur durch Typumwandlung möglich ist Object Werte, die ich hoffentlich vollständig vermeiden würde, es sei denn, es gibt eine Möglichkeit, dies einmal sicher zu tun und dies für alle Summentypen verwenden zu können.
Scala verwendet Vererbung, um Summentypen zu emulieren. Sie können Java nicht dazu zwingen, einen Typ “versiegelt” zu machen (das heißt, jemand anderes könnte kommen und etwas Dummes schreiben wie class Apollo11 extends Either<Integer, Integer>), aber Sie können einfach keine Unterklassen mehr selbst schreiben und das Beste hoffen. Dies “sicher” zu tun, liegt außerhalb der Möglichkeiten des Java-Typsystems, aber es ist mit Vererbung möglich.
– Silvio Mayolo
8. Januar 2018 um 1:45 Uhr
Betrachtet man die Signatur von Collections.newSetFromMap() Ich denke, es sollte vollkommen in Ordnung sein, nur einen beliebigen generischen Typ zu definieren, wie z class Left<L> extends Either<L, Object>.
– Isruo
8. Januar 2018 um 1:46 Uhr
@Dragonthoughts Ja, aber wir möchten es eine begrenzte Anzahl von Malen verlängern und es dann sperren. Scala hat dies in Form der sealed Schlüsselwort, aber in Java lassen Sie eine Klasse entweder für alle offen oder erstellen sie final und sogar sich selbst daran hindern, zu erben.
– Silvio Mayolo
8. Januar 2018 um 1:49 Uhr
Indem Sie alle Konstruktoren erstellen package-private, kann die Vererbung jedoch auf das umschließende Paket beschränkt werden. Die Instanziierung muss dann über die statische Fabrik erfolgen, wie es hier bereits getan wird.
– Isruo
8. Januar 2018 um 1:52 Uhr
Sie könnten eine Basis haben, die nur im Paket sichtbar ist, aber die abgeleiteten Klassen final und public haben.
– Drachengedanken
8. Januar 2018 um 1:54 Uhr
gdejohn
Machen Either eine abstrakte Klasse ohne Felder und nur einen Konstruktor (privat, ohne Argumente, leer) und verschachteln Sie Ihre “Datenkonstruktoren” (left und right statische Factory-Methoden) innerhalb der Klasse, sodass sie den privaten Konstruktor sehen können, aber sonst nichts, wodurch der Typ effektiv versiegelt wird.
Verwenden Sie eine abstrakte Methode either um einen erschöpfenden Musterabgleich zu simulieren, wobei die konkreten Typen, die von den statischen Factory-Methoden zurückgegeben werden, entsprechend überschrieben werden. Implementieren Sie Convenience-Methoden (wie fromLeft, fromRight, bimap, first, second) bezüglich either.
import java.util.Optional;
import java.util.function.Function;
public abstract class Either<A, B> {
private Either() {}
public abstract <C> C either(Function<? super A, ? extends C> left,
Function<? super B, ? extends C> right);
public static <A, B> Either<A, B> left(A value) {
return new Either<A, B>() {
@Override
public <C> C either(Function<? super A, ? extends C> left,
Function<? super B, ? extends C> right) {
return left.apply(value);
}
};
}
public static <A, B> Either<A, B> right(B value) {
return new Either<A, B>() {
@Override
public <C> C either(Function<? super A, ? extends C> left,
Function<? super B, ? extends C> right) {
return right.apply(value);
}
};
}
public Optional<A> fromLeft() {
return this.either(Optional::of, value -> Optional.empty());
}
}
Angenehm und sicher! Keine Möglichkeit, es zu vermasseln. Da der Typ effektiv versiegelt ist, können Sie sicher sein, dass es immer nur zwei Fälle geben wird und jede Operation letztendlich in Bezug auf die definiert werden muss either -Methode, die den Aufrufer zwingt, beide Fälle zu behandeln.
In Bezug auf das Problem, das Sie zu lösen versuchten class Left<L> extends Either<L,?>betrachten Sie die Signatur <A, B> Either<A, B> left(A value). Der Typparameter B erscheint nicht in der Parameterliste. Also, wenn ein Wert irgendeiner Art gegeben ist Akönnen Sie eine bekommen Either<A, B> zum irgendein Typ B.
Ich würde hier eine Aufzählung einführen, um eine Möglichkeit zu bieten, die Fälle vollständig abzugleichen
– Alexander
8. Januar 2018 um 19:24 Uhr
Es kann sinnvoll sein, die Methoden-Generika anders zu benennen als die Generika der Klasse. ZB vielleicht AL, BL, ARund BR damit man leicht unterscheiden kann, welche wo verwendet werden.
– jpmc26
9. Januar 2018 um 0:01 Uhr
Beachten Sie, dass dieser Ansatz mit einem JSR269-Annotationsprozessor industrialisiert werden kann, um die Boilerplate zu generieren (die statischen linken und rechten Methoden und andere, wie hashCode/equals). Siehe mein Projekt Ableiten4J.
– JbGi
9. Januar 2018 um 11:10 Uhr
@vtosh das lag daran, dass ich die Typargument-Inferenz der anonymen Klasse verwendet habe, die in Java 9 hinzugefügt wurde, aber ich habe sie bearbeitet, um die Typargumente explizit zu machen, sodass sie jetzt auf Java 8 kompiliert werden
– gdejohn
10. Dezember 2019 um 17:23 Uhr
Jon Purdy
Eine Standardmethode zum Codieren von Summentypen ist die Boehm-Berarducci-Codierung (oft mit dem Namen ihres Cousins, Church-Codierung, bezeichnet), die einen algebraischen Datentyp als seinen darstellt Eliminator, dh eine Funktion, die einen Mustervergleich durchführt. In Haskel:
left :: a -> (a -> r) -> (b -> r) -> r
left x l _ = l x
right :: b -> (a -> r) -> (b -> r) -> r
right x _ r = r x
match :: (a -> r) -> (b -> r) -> ((a -> r) -> (b -> r) -> r) -> r
match l r k = k l r
-- Or, with a type synonym for convenience:
type Either a b r = (a -> r) -> (b -> r) -> r
left :: a -> Either a b r
right :: b -> Either a b r
match :: (a -> r) -> (b -> r) -> Either a b r -> r
In Java würde dies wie ein Besucher aussehen:
public interface Either<A, B> {
<R> R match(Function<A, R> left, Function<B, R> right);
}
public final class Left<A, B> implements Either<A, B> {
private final A value;
public Left(A value) {
this.value = value;
}
public <R> R match(Function<A, R> left, Function<B, R> right) {
return left.apply(value);
}
}
public final class Right<A, B> implements Either<A, B> {
private final B value;
public Right(B value) {
this.value = value;
}
public <R> R match(Function<A, R> left, Function<B, R> right) {
return right.apply(value);
}
}
Beispielnutzung:
Either<Integer, String> result = new Left<Integer, String>(42);
String message = result.match(
errorCode -> "Error: " + errorCode.toString(),
successMessage -> successMessage);
Der Einfachheit halber können Sie eine Fabrik zum Erstellen erstellen Left und Right Werte, ohne jedes Mal die Typparameter erwähnen zu müssen; Sie können auch eine Version von hinzufügen match das akzeptiert Consumer<A> left, Consumer<B> right Anstatt von Function<A, R> left, Function<B, R> right wenn Sie die Möglichkeit des Musterabgleichs wünschen, ohne ein Ergebnis zu erzeugen.
Dies spiegelt bereits den Rest meiner Implementierung wider, was ich darin versteckt hatte ...da es an das eigentliche Problem der Implementierung des Summentyps angrenzt, aber definitiv ein wichtiger Teil der Implementierung des Typs „Beide“ ist
– Zoey Hewll
8. Januar 2018 um 3:14 Uhr
Ein Hauptverkaufsargument dieses Ansatzes ist, dass keine Typumwandlungen (nor instanceof), um den Summentyp zu eliminieren. Mit anderen Worten, dies würde auch in einem Fragment von Java funktionieren, in dem potenziell gefährliche Konstrukte wie Typumwandlungen verboten sind.
Okay, also ist die Vererbungslösung definitiv die erfolgversprechendste. Das, was wir tun möchten, ist class Left<L> extends Either<L, ?>, was wir aufgrund der generischen Regeln von Java leider nicht tun können. Wenn wir jedoch die Zugeständnisse machen, die die Art von Left oder Right muss die “alternative” Möglichkeit kodieren, wir können dies tun.
public class Left<L, R> extends Either<L, R>`
Nun möchten wir gerne konvertieren Left<Integer, A> zu Left<Integer, B>da es das eigentlich nicht tut verwenden dieser zweite Typparameter. Wir können eine Methode definieren, um diese Konvertierung intern durchzuführen und so diese Freiheit in das Typsystem zu codieren.
public <R1> Left<L, R1> phantom() {
return new Left<L, R1>(contents);
}
Vollständiges Beispiel:
public class EitherTest {
public abstract static class Either<L, R> {}
public static class Left<L, R> extends Either<L, R> {
private L contents;
public Left(L x) {
contents = x;
}
public <R1> Left<L, R1> phantom() {
return new Left<L, R1>(contents);
}
}
public static class Right<L, R> extends Either<L, R> {
private R contents;
public Right(R x) {
contents = x;
}
public <L1> Right<L1, R> phantom() {
return new Right<L1, R>(contents);
}
}
}
Natürlich möchten Sie einige Funktionen hinzufügen, um tatsächlich auf die Inhalte zuzugreifen und um zu überprüfen, ob ein Wert vorhanden ist Left oder Right du musst also nicht spritzen instanceof und explizite Besetzungen überall, aber das sollte zumindest für den Anfang ausreichen.
Welche Rolle spielt die Umschließung EitherTest Typ?
– Zoey Hewll
8. Januar 2018 um 2:03 Uhr
Nichts Bestimmtes. Packen Sie einfach alles in eine Datei, damit alle Klassen öffentlich sein können. Java erfordert, dass jede Datei höchstens eine öffentliche Klasse hat. Wenn Sie diese Lösung in Ihrem Projekt implementieren, gibt es keinen Grund dafür EitherTest.
– Silvio Mayolo
8. Januar 2018 um 2:05 Uhr
Alexander
Nachlass kann verwendet werden, um Summentypen (disjunkte Vereinigungen) zu emulieren, aber es gibt ein paar Probleme, mit denen Sie sich befassen müssen:
Sie müssen darauf achten, andere davon abzuhalten, Ihrem Typ neue Fälle hinzuzufügen. Dies ist besonders wichtig, wenn Sie jeden Fall, auf den Sie stoßen, erschöpfend behandeln möchten. Dies ist mit einer nicht finalen Superklasse und einem paketprivaten Konstruktor möglich.
Das Fehlen von Pattern-Patching macht es ziemlich schwierig, einen Wert dieses Typs zu konsumieren. Wenn Sie eine vom Compiler geprüfte Methode wünschen, um sicherzustellen, dass Sie alle Fälle vollständig behandelt haben, müssen Sie selbst eine Match-Funktion implementieren.
Sie werden zu einem von zwei API-Stilen gezwungen, von denen keiner ideal ist:
Alle Fälle implementieren eine gemeinsame API und werfen Fehler auf APIs, die sie selbst nicht unterstützen. In Betracht ziehen Optional.get(). Idealerweise wäre diese Methode nur für einen disjunkten Typ verfügbar, dessen Wert bekannt ist some statt none. Aber es gibt keine Möglichkeit, das zu tun, also ist es ein Instanzmitglied eines Generals Optional Typ. Es wirft NoSuchElementException wenn Sie es auf einem optionalen aufrufen, dessen “Fall” “none” ist.
Jeder Fall hat eine eindeutige API, die Ihnen genau sagt, wozu er fähig ist, aber das erfordert eine manuelle Typprüfung und jedes Mal, wenn Sie eine dieser unterklassenspezifischen Methoden aufrufen möchten.
Das Ändern von “Fällen” erfordert eine neue Objektzuweisung (und erhöht den Druck auf den GC, wenn dies häufig geschieht).
TL;DR: Funktionale Programmierung in Java ist keine angenehme Erfahrung.
Lassen Sie mich eine ganz andere Lösung vorschlagen, die keine Vererbung / abstrakte Klassen / Schnittstellen verwendet. Auf der anderen Seite erfordert es einige Anstrengungen für jeden neu definierten “Summentyp”. Ich denke jedoch, dass es viele Vorteile hat: Es ist sicher, es verwendet nur grundlegende Konzepte, es fühlt sich natürlich an und lässt mehr als 2 “Untertypen” zu.
Hier ist ein Proof of Concept für binäre Bäume, weil es praktischer ist als “Either”, aber Sie können einfach die Kommentare als Richtlinie verwenden, um Ihren eigenen Summentyp zu erstellen.
public class Tree {
// 1) Create an enum listing all "subtypes" (there may be more than 2)
enum Type { Node, Leaf }
// 2) Create a static class for each subtype (with the same name for clarity)
public static class Node {
Tree l,r;
public Node(Tree l, Tree r) {
this.l = l;
this.r = r;
}
}
public static class Leaf {
int label;
public Leaf(int label) {
this.label = label;
}
}
// 3) Each instance must have:
// One variable to indicate which subtype it corresponds to
Type type;
// One variable for each of the subtypes (only one will be different from null)
Leaf leaf;
Node node;
// 4) Create one constructor for each subtype (it could even be private)
public Tree(Node node) {
this.type = Type.Node;
this.node = node;
}
// 5) Create one "factory" method for each subtype (not mandatory but quite convenient)
public static Tree newNode(Tree l, Tree r) {
return new Tree(new Node(l,r));
}
public Tree(Leaf leaf) {
this.type = Type.Leaf;
this.leaf = leaf;
}
public static Tree newLeaf(int label) {
return new Tree(new Leaf(label));
}
// 6) Create a generic "matching" function with one argument for each subtype
// (the constructors ensure that no "null pointer exception" can be raised)
public <T> T match(Function<Node,T> matchNode, Function<Leaf,T> matchLeaf) {
switch (type) {
case Node:
return matchNode.apply(node);
case Leaf:
return matchLeaf.apply(leaf);
}
return null;
}
// 7) Have fun !
// Note that matchings are quite natural to write.
public int size() {
return match(
node -> 1 + node.l.size() + node.r.size(),
leaf -> 1
);
}
public String toString() {
return match(
node -> {
String sl = node.l.toString();
String sr = node.r.toString();
return "Node { "+sl+" , "+sr+" }";
},
leaf -> "Leaf: "+leaf.label
);
}
public static void main(String [] args) {
Tree node1 = Tree.newNode(Tree.newLeaf(1),Tree.newLeaf(2));
Tree node2 = Tree.newNode(node1,Tree.newLeaf(3));
System.out.println(node2.size());
System.out.println(node2);
}
}
Kritik darf gerne geäußert werden, ich interessiere mich wirklich für dieses Thema und würde mich freuen, mehr zu erfahren.
Scala verwendet Vererbung, um Summentypen zu emulieren. Sie können Java nicht dazu zwingen, einen Typ “versiegelt” zu machen (das heißt, jemand anderes könnte kommen und etwas Dummes schreiben wie
class Apollo11 extends Either<Integer, Integer>
), aber Sie können einfach keine Unterklassen mehr selbst schreiben und das Beste hoffen. Dies “sicher” zu tun, liegt außerhalb der Möglichkeiten des Java-Typsystems, aber es ist mit Vererbung möglich.– Silvio Mayolo
8. Januar 2018 um 1:45 Uhr
Betrachtet man die Signatur von
Collections.newSetFromMap()
Ich denke, es sollte vollkommen in Ordnung sein, nur einen beliebigen generischen Typ zu definieren, wie zclass Left<L> extends Either<L, Object>
.– Isruo
8. Januar 2018 um 1:46 Uhr
@Dragonthoughts Ja, aber wir möchten es eine begrenzte Anzahl von Malen verlängern und es dann sperren. Scala hat dies in Form der
sealed
Schlüsselwort, aber in Java lassen Sie eine Klasse entweder für alle offen oder erstellen siefinal
und sogar sich selbst daran hindern, zu erben.– Silvio Mayolo
8. Januar 2018 um 1:49 Uhr
Indem Sie alle Konstruktoren erstellen
package-private
, kann die Vererbung jedoch auf das umschließende Paket beschränkt werden. Die Instanziierung muss dann über die statische Fabrik erfolgen, wie es hier bereits getan wird.– Isruo
8. Januar 2018 um 1:52 Uhr
Sie könnten eine Basis haben, die nur im Paket sichtbar ist, aber die abgeleiteten Klassen final und public haben.
– Drachengedanken
8. Januar 2018 um 1:54 Uhr