it-swarm.com.de

Wie codieren Sie algebraische Datentypen in einer C # - oder Java-ähnlichen Sprache?

Es gibt einige Probleme, die durch algebraische 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 befindet sich in Haskell, ist jedoch in anderen Sprachen mit nativer Unterstützung für algebraische Datentypen ähnlich.

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 Unterklasse. 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 ein Weg zu versiegeln Klassen, d. H. Ein Weg, um das Hinzufügen von Unterklassen zu einer Hierarchie unmöglich zu machen.

Wie würden Sie dieses Problem in einer Sprache wie C # oder Java angehen? Die beiden Stolpersteine, die ich beim Versuch gefunden habe, algebraische Datentypen in C # zu verwenden, waren:

  • Ich konnte nicht herausfinden, wie der unterste Typ in C # heißt (d. H. Ich konnte nicht herausfinden, was in class Empty : ConsList< ??? > Eingefügt werden soll).
  • Ich konnte keinen Weg finden, um SiegelConsList, so dass 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?

61
Jörg W Mittag

Es gibt eine einfache, aber schwierige Möglichkeit, Klassen in Java zu versiegeln. Sie fügen einen privaten Konstruktor in die Basisklasse ein und erstellen daraus Unterklassen zu inneren Klassen.

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

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

43
James Iry

Sie können dies erreichen, indem Sie Besuchermuster verwenden, das den Mustervergleich ergänzt. Zum Beispiel

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

kann geschrieben werden in 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);
    }
}

Die Versiegelung wird durch die Klasse Visitor erreicht. Jede ihrer Methoden gibt an, wie eine der Unterklassen dekonstruiert werden soll. Sie könnten weitere Unterklassen hinzufügen, müssten jedoch accept implementieren und eine der visit... Methoden, also müsste es sich entweder wie Cons oder wie Nil verhalten.

20
Petr Pudlák

Wenn Sie C # -benannte Parameter missbrauchen (eingeführt in C # 4.0), können Sie algebraische Datentypen erstellen, die leicht zuzuordnen sind:

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

Hier ist die Implementierung der Klasse 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

In C # kann dieser Typ Empty nicht verwendet werden, da die Basistypen aufgrund der erneuten Bestätigung für verschiedene Elementtypen unterschiedlich sind. Sie können nur Empty<T> Haben; nicht so nützlich.

In Java können Sie aufgrund der Typlöschung Empty : ConsList Haben, aber ich bin nicht sicher, ob die Typprüfung nicht irgendwo schreien würde.

Da jedoch beide Sprachen null haben, können Sie sich alle ihre Referenztypen als "Whatever | Null" vorstellen. Sie würden also einfach das null als "leer" verwenden, um zu vermeiden, dass Sie angeben müssen, was es ableitet.

5
Jan Hudec

Das einzige, was über die naive Unterklasse hinaus benötigt wird, ist eine Möglichkeit, Klassen zu versiegeln, d. H. Eine Möglichkeit, das Hinzufügen von Unterklassen zu einer Hierarchie unmöglich zu machen.

In Java können Sie nicht. Sie können die Basisklasse jedoch als Paket privat deklarieren, was bedeutet, dass alle direkten Unterklassen zum selben Paket wie die Basisklasse gehören müssen. Wenn Sie dann die deklarieren Unterklassen als endgültig, sie können nicht weiter untergeordnet werden.

Ich weiß nicht, ob dies Ihr wirkliches Problem lösen würde ...

3
Stephen C

Der Datentyp ConsList<A> kann als Schnittstelle dargestellt werden. Die Schnittstelle stellt eine einzelne deconstruct -Methode bereit, mit der Sie einen Wert dieses Typs "dekonstruieren" können, dh jeden der möglichen Konstruktoren behandeln können. Aufrufe einer deconstruct -Methode sind analog zu einer case of Form in Haskell oder ML.

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

Die Methode deconstruct verwendet für jeden Konstruktor in der ADT eine "Rückruffunktion". In unserem Fall übernimmt es eine Funktion für den Fall der leeren Liste und eine andere Funktion für den Fall "cons cell".

Jede Rückruffunktion akzeptiert als Argumente die Werte, die vom Konstruktor akzeptiert werden. Der Fall "leere Liste" akzeptiert also keine Argumente, aber der Fall "cons cell" akzeptiert zwei Argumente: den Kopf und das Ende der Liste.

Wir können diese "Mehrfachargumente" mit Tuple Klassen oder mit Curry codieren. In diesem Beispiel habe ich mich für eine einfache Pair -Klasse entschieden.

Die Schnittstelle wird für jeden Konstruktor einmal implementiert. Zunächst haben wir die Implementierung für die "leere Liste". Die Implementierung von deconstruct ruft einfach die Rückruffunktion emptyCase auf.

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

Dann implementieren wir den Fall "cons cell" auf ähnliche Weise. Diesmal hat die Klasse Eigenschaften: Kopf und Ende der nicht leeren Liste. In der Implementierung von deconstruct werden diese Eigenschaften an die Rückruffunktion consCase übergeben.

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

Hier ist ein Beispiel für die Verwendung dieser Codierung von ADTs: Wir können eine reduce -Funktion schreiben, die die übliche Fold-Over-Liste ist.

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

Dies ist analog zu dieser Implementierung in 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

Das einzige, was über die naive Unterklasse hinaus benötigt wird, ist eine Möglichkeit, Klassen zu versiegeln, d. H. Eine Möglichkeit, das Hinzufügen von Unterklassen zu einer Hierarchie unmöglich zu machen.

Wie würden Sie dieses Problem in einer Sprache wie C # oder Java angehen?

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 das so etwas wie

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

In C # ist es aufgrund der reifizierten Generika komplizierter - der einfachste Ansatz könnte darin bestehen, den Typ in einen String zu konvertieren und diesen zu entstellen.

Beachten Sie, dass in Java auch dieser Mechanismus theoretisch von jemandem umgangen werden kann, der dies wirklich über das Serialisierungsmodell oder Sun.misc.Unsafe Wollen möchte.

2
Peter Taylor