web-dev-qa-db-fra.com

Types d'union/somme marqués Java

Est-il possible de définir un type de somme en Java? Java semble naturellement prendre en charge directement les types de produits, et je pensais que les enums pourraient lui permettre de prendre en charge les types de somme, et l'héritage semble peut-être le faire, mais il existe au moins un cas que je ne parviens pas à résoudre . un type sum est un type qui peut avoir exactement l'un des types différents, comme une union marquée dans C . Dans mon cas, j'essaie d'implémenter le type Ek de haskell en Java:

data Either a b = Left a | Right b

mais au niveau de la base, je dois l'implémenter en tant que type de produit et ignorer l'un de ses champs:

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");
        }
    }

    .
    .
    .
}

J'ai essayé d'implémenter cela avec l'héritage, mais je dois utiliser un paramètre de type caractère générique, ou un équivalent, que les génériques Java n'autorisent pas:

public class Left<L> extends Either<L,?>

Je n'ai pas beaucoup utilisé Java Enums, mais, même s'ils semblent le prochain meilleur candidat, je ne suis pas optimiste.
À ce stade, je pense que cela pourrait être possible uniquement en transtypant les valeurs Object, que je souhaiterais éviter complètement, à moins qu'il soit possible de le faire une fois, en toute sécurité, et de pouvoir l'utiliser pour tous les types de somme. .

36
Zoey Hewll

Faites de Either une classe abstraite avec un constructeur privé et imbriquez vos "constructeurs de données" (méthodes left et right de fabrique statique) dans la classe de manière à ce qu'ils puissent voir le constructeur privé, mais rien d'autre ne puisse sceller efficacement le type.

Utilisez une méthode abstraite either pour simuler une correspondance de modèle exhaustive, en remplaçant correctement les types concrets renvoyés par les méthodes de fabrique statique. Implémentez des méthodes pratiques (comme fromLeft , fromRight , bimap , first , second ) en termes de 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<>() {
            @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<>() {
            @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());
    }

    // other convenience methods
}

Agréable et sécuritaire! Pas moyen de tout foirer.

En ce qui concerne le problème que vous avez tenté de résoudre, class Left<L> extends Either<L,?>, considérez la signature <A, B> Either<A, B> left(A value). Le paramètre de type B n'apparaît pas dans la liste des paramètres. Donc, étant donné une valeur de type A, vous pouvez obtenir un Either<A, B> pour any type B.

49
gdejohn

Un moyen standard de codage des types de somme est le codage de Boehm – Berarducci (souvent désigné par le nom de son cousin, le codage de Church) qui représente un type de données algébrique en tant que son éliminateur , c'est-à-dire une fonction qui effectue une correspondance de modèle. En Haskell:

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

En Java, cela ressemblerait à un visiteur:

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);
    }

}

Exemple d'utilisation:

Either<Integer, String> result = new Left<Integer, String>(42);
String message = result.match(
  errorCode -> "Error: " + errorCode.toString(),
  successMessage -> successMessage);

Pour plus de commodité, vous pouvez créer une usine permettant de créer les valeurs Left et Right sans avoir à mentionner les paramètres de type à chaque fois; vous pouvez également ajouter une version de match qui accepte Consumer<A> left, Consumer<B> right au lieu de Function<A, R> left, Function<B, R> right si vous souhaitez utiliser l'option de correspondance de modèle sans produire de résultat.

19
Jon Purdy

Bon, la solution d'héritage est certainement la plus prometteuse. Ce que nous aimerions faire, c’est class Left<L> extends Either<L, ?>, ce que nous ne pouvons malheureusement pas faire à cause des règles génériques de Java. Toutefois, si nous faisons des concessions selon lesquelles le type de Left ou Right doit coder la possibilité "alternative", nous pouvons le faire.

public class Left<L, R> extends Either<L, R>`

Maintenant, nous aimerions pouvoir convertir Left<Integer, A> en Left<Integer, B>, car il n’a pas réellement utilise ce second paramètre de type. Nous pouvons définir une méthode pour effectuer cette conversion en interne, codant ainsi cette liberté dans le système de types.

public <R1> Left<L, R1> phantom() {
  return new Left<L, R1>(contents);
}

Exemple complet:

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);
    }

  }

}

Bien sûr, vous voudrez ajouter quelques fonctions pour accéder réellement au contenu et pour vérifier si une valeur est Left ou Right afin que vous n'ayez pas à saupoudrer instanceof et des conversions explicites partout, mais cela devrait être suffisant pour commencer, tout au moins.

6
Silvio Mayolo

Héritage peut être utilisé pour émuler des types de somme (Unions disjointes), mais vous devez résoudre quelques problèmes:

  1. Vous devez veiller à empêcher les autres d’ajouter de nouveaux cas à votre type. Ceci est particulièrement important si vous souhaitez traiter de manière exhaustive tous les cas que vous pourriez rencontrer. C'est possible avec une super classe non finale et un constructeur de paquet privé.
  2. L'absence de correction de motif rend assez difficile l'utilisation d'une valeur de ce type. Si vous voulez que le compilateur vérifie que vous avez traité tous les cas de manière exhaustive, vous devez implémenter vous-même une fonction de correspondance.
  3. Vous êtes forcé de choisir l'un des deux styles d'API, qui ne sont ni l'idéal:
    • Tous les cas implémentent une API commune, générant des erreurs sur des API non prises en charge par eux-mêmes. Considérez Optional.get() . Idéalement, cette méthode ne serait disponible que sur un type disjoint dont la valeur est connue comme étant some plutôt que none. Mais il n'y a aucun moyen de le faire, c'est donc un membre d'instance d'un type Optional général. NoSuchElementException est jeté si vous l'appelez avec une option dont le "cas" est "none".
    • Chaque cas a une API unique qui vous dit exactement de quoi il est capable, mais cela nécessite une vérification de type manuelle et un transtypage à chaque fois que vous souhaitez appeler l'une de ces méthodes spécifiques à une sous-classe.
  4. Changer les "cas" nécessite une nouvelle allocation d'objets (et ajoute une pression sur le GC si cela est fait souvent).

TL; DR: La programmation fonctionnelle en Java n’est pas une expérience agréable.

1
Alexander