web-dev-qa-db-fra.com

Comment coder les types de données algébriques dans un langage de type C # ou Java?

Il existe certains problèmes qui sont facilement résolus par les types de données algébriques, par exemple, un type de liste peut être exprimé très succinctement comme suit:

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

Cet exemple particulier se trouve dans Haskell, mais il serait similaire dans d'autres langues avec une prise en charge native des types de données algébriques.

Il s'avère qu'il existe une correspondance évidente avec le sous-typage de style OO: le type de données devient une classe de base abstraite et chaque constructeur de données devient une sous-classe concrète. Voici un exemple dans 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+)

La seule chose nécessaire au-delà du sous-classement naïf est un moyen de sceller classes, c'est-à-dire un moyen de rendre impossible l'ajout de sous-classes à une hiérarchie.

Comment aborderiez-vous ce problème dans un langage comme C # ou Java? Les deux pierres d'achoppement que j'ai trouvées en essayant d'utiliser les types de données algébriques en C # étaient:

  • Je ne pouvais pas comprendre comment le type inférieur est appelé en C # (c'est-à-dire que je ne pouvais pas savoir quoi mettre dans class Empty : ConsList< ??? >)
  • Je n'ai pas réussi à trouver un moyen de scellerConsList afin qu'aucune sous-classe ne puisse être ajoutée à la hiérarchie

Quelle serait la façon la plus idiomatique d'implémenter des types de données algébriques en C # et/ou Java? Ou, si ce n'est pas possible, quel serait le remplacement idiomatique?

61
Jörg W Mittag

Il existe un moyen simple, mais standard, de sceller des classes en Java. Vous mettez un constructeur privé dans la classe de base, puis vous en créez des sous-classes.

public abstract class List<A> {

   // private constructor is uncallable by any sublclasses except inner classes
   private List() {
   }

   public static final class Nil<A> extends List<A> {
   }

   public static final class Cons<A> extends List<A> {
      public final A head;
      public final List<A> tail;

      public Cons(A head, List<A> tail) {
         this.head = head;
         this.tail = tail;
      }
   }
}

Tack sur un modèle de visiteur pour l'expédition.

Mon projet jADT: Java Algebraic DataTypes génère tout ce passe-partout pour vous https://github.com/JamesIry/jADT

43
James Iry

Vous pouvez y parvenir en utilisant le modèle visiteur , qui complètera la correspondance des modèles. Par exemple

data List a = Nil | Cons { value :: a, sublist :: List a }

peut être écrit en Java as

interface List<T> {
    public <R> R accept(Visitor<T,R> visitor);

    public static interface Visitor<T,R> {
        public R visitNil();
        public R visitCons(T value, List<T> sublist);
    }
}

final class Nil<T> implements List<T> {
    public Nil() { }

    public <R> R accept(Visitor<T,R> visitor) {
        return visitor.visitNil();
    }
}
final class Cons<T> implements List<T> {
    public final T value;
    public final List<T> sublist;

    public Cons(T value, List<T> sublist) {
        this.value = value;
        this.sublist = sublist;
    }

    public <R> R accept(Visitor<T,R> visitor) {
        return visitor.visitCons(value, sublist);
    }
}

L'étanchéité est réalisée par la classe Visitor. Chacune de ses méthodes déclare comment déconstruire l'une des sous-classes. Vous pourriez ajouter plus de sous-classes, mais il faudrait implémenter accept et en appelant l'un des visit..., il devrait donc se comporter comme Cons ou comme Nil.

20
Petr Pudlák

Si vous abusez des paramètres nommés C # (introduits dans C # 4.0), vous pouvez créer des types de données algébriques faciles à mettre en correspondance:

Either<string, string> e = MonthName(2);

// Match with no return value.
e.Match
(
    Left: err => { Console.WriteLine("Could not convert month: {0}", err); },
    Right: name => { Console.WriteLine("The month is {0}", name); }
);

// Match with a return value.
string monthName =
    e.Match
    (
        Left: err => null,
        Right: name => name
    );
Console.WriteLine("monthName: {0}", monthName);

Voici l'implémentation de la classe Either:

public abstract class Either<L, R>
{
    // Subclass implementation calls the appropriate continuation.
    public abstract T Match<T>(Func<L, T> Left, Func<R, T> Right);

    // Convenience wrapper for when the caller doesn't want to return a value
    // from the match expression.
    public void Match(Action<L> Left, Action<R> Right)
    {
        this.Match<int>(
            Left: x => { Left(x); return 0; },
            Right: x => { Right(x); return 0; }
        );
    }
}

public class Left<L, R> : Either<L, R>
{
    L Value {get; set;}

    public Left(L Value)
    {
        this.Value = Value;
    }

    public override T Match<T>(Func<L, T> Left, Func<R, T> Right)
    {
        return Left(Value);
    }
}

public class Right<L, R> : Either<L, R>
{
    R Value { get; set; }

    public Right(R Value)
    {
        this.Value = Value;
    }

    public override T Match<T>(Func<L, T> Left, Func<R, T> Right)
    {
        return Right(Value);
    }
}
13
Joey Adams

En C #, vous ne pouvez pas avoir ce type Empty, car, en raison de la réification, les types de base sont différents pour différents types de membres. Vous ne pouvez avoir que Empty<T>; pas très utile.

En Java, vous pouvez avoir Empty : ConsList en raison de l'effacement du type, mais je ne sais pas si le vérificateur de type ne crierait pas quelque part.

Cependant, étant donné que les deux langues ont null, vous pouvez considérer tous leurs types de référence comme étant "Quoi que | Null". Il vous suffit donc d'utiliser le null comme "Empty" pour éviter d'avoir à spécifier ce qu'il dérive.

5
Jan Hudec

La seule chose nécessaire au-delà du sous-classement naïf est un moyen de sceller les classes, c'est-à-dire un moyen de rendre impossible l'ajout de sous-classes à une hiérarchie.

Dans Java vous ne pouvez pas. Mais vous pouvez déclarer la classe de base comme package privé, ce qui signifie que toutes les sous-classes directes doivent appartenir au même package que la classe de base. Si vous déclarez ensuite le sous-classes comme final, elles ne peuvent plus être sous-classées.

Je ne sais pas si cela résoudrait votre vrai problème ...

3
Stephen C

Le type de données ConsList<A> peut être représenté comme une interface. L'interface expose une seule méthode deconstruct qui vous permet de "déconstruire" une valeur de ce type - c'est-à-dire de gérer chacun des constructeurs possibles. Les appels à une méthode deconstruct sont analogues à un case of forme en Haskell ou ML.

interface ConsList<A> {
  <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  );
}

La méthode deconstruct prend une fonction "callback" pour chaque constructeur dans l'ADT. Dans notre cas, il faut une fonction pour le cas de liste vide, et une autre fonction pour le cas "contre cellule".

Chaque fonction de rappel accepte comme arguments les valeurs acceptées par le constructeur. Ainsi, le cas "liste vide" ne prend aucun argument, mais le cas "contre-cellule" prend deux arguments: la tête et la queue de la liste.

Nous pouvons encoder ces "arguments multiples" en utilisant les classes Tuple, ou en utilisant le currying. Dans cet exemple, j'ai choisi d'utiliser une simple classe Pair.

L'interface est implémentée une fois pour chaque constructeur. Tout d'abord, nous avons l'implémentation de la "liste vide". L'implémentation deconstruct appelle simplement la fonction de rappel emptyCase.

class ConsListEmpty<A> implements ConsList<A> {
  public ConsListEmpty() {}

  public <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  ) {
    return emptyCase.apply(new Unit());
  }
}

Ensuite, nous implémentons le cas "contre-cellule" de manière similaire. Cette fois, la classe a des propriétés: la tête et la queue de la liste non vide. Dans l'implémentation deconstruct, ces propriétés sont passées à la fonction de rappel consCase.

class ConsListConsCell<A> implements ConsList<A> {
  private A head;
  private ConsList<A> tail;

  public ConsListCons(A head, ConsList<A> tail) {
    this.head = head;
    this.tail = tail;
  }

  public <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  ) {
    return consCase.apply(new Pair<A,ConsList<A>>(this.head, this.tail));
  }
}

Voici un exemple d'utilisation de ce codage d'ADT: nous pouvons écrire une fonction reduce qui est la liste déroulante habituelle.

<T> T reduce(Function<Pair<T,A>,T> reducer, T initial, ConsList<T> l) {
  return l.deconstruct(
    ((unit) -> initial),
    ((t) -> reduce(reducer, reducer.apply(initial, t.v1), t.v2))
  );
}

Ceci est analogue à cette implémentation dans Haskell:

reduce reducer initial l = case l of
  Empty -> initial
  Cons t_v1 t_v2  -> reduce reducer (reducer initial t_v1) t_v2
3
jameshfisher

La seule chose nécessaire au-delà du sous-classement naïf est un moyen de sceller les classes, c'est-à-dire un moyen de rendre impossible l'ajout de sous-classes à une hiérarchie.

Comment aborderiez-vous ce problème dans un langage comme C # ou Java?

Il n'y a pas de bon moyen de le faire, mais si vous êtes prêt à vivre avec un hack hideux, vous pouvez ajouter une vérification de type explicite au constructeur de la classe de base abstraite. En Java, ce serait quelque chose comme

protected ConsList() {
    Class<?> clazz = getClass();
    if (clazz != Empty.class && clazz != ConsCell.class) throw new Exception();
}

En C #, c'est plus compliqué à cause des génériques réifiés - l'approche la plus simple pourrait être de convertir le type en chaîne et de le modifier.

Notez que dans Java même ce mécanisme peut théoriquement être contourné par quelqu'un qui veut vraiment via le modèle de sérialisation ou Sun.misc.Unsafe.

2
Peter Taylor