web-dev-qa-db-ja.com

代数的データ型をC#またはJavaのような言語でどのようにエンコードしますか?

代数的データ型で簡単に解決できる問題がいくつかあります。たとえば、リスト型は次のように非常に簡潔に表現できます。

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

この特定の例はHaskellにありますが、代数的データ型をネイティブでサポートしている他の言語でも同様です。

OOスタイルのサブタイピングへの明らかなマッピングがあることがわかります。データ型は抽象基本クラスになり、すべてのデータコンストラクターは具象サブクラスになります。以下は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+)

単純なサブクラス化以外に必要なことは、クラスをsealにする方法、つまりサブクラスを階層に追加できないようにする方法です。

C#やJavaなどの言語でこの問題にどのように取り組みますか? C#で代数的データ型を使用しようとしたときに見つけた2つの障害は次のとおりです。

  • C#で下部の型が何と呼ばれているかを理解できませんでした(つまり、class Empty : ConsList< ??? >に何を入力するかを理解できませんでした)
  • sealConsListにサブクラスを階層に追加できないようにする方法を理解できませんでした

C#やJavaで代数的データ型を実装する最も慣用的な方法は何でしょうか?または、それが不可能な場合、慣用的な置き換えは何でしょうか?

61
Jörg W Mittag

簡単ですが、Javaでクラスをシールするためのヘビーな方法があります。基本クラスにプライベートコンストラクターを配置し、その内部クラスをサブクラス化します。

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

発送用の訪問者パターンにタックします。

私のプロジェクトjADT:Java Algebraic DataTypesがすべてのボイラープレートを生成します https://github.com/JamesIry/jADT

43
James Iry

これは、パターンマッチングを補足する visitor pattern を使用して実現できます。例えば

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

Java

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

シーリングはVisitorクラスによって実現されます。その各メソッドは、サブクラスの1つを分解する方法を宣言します。さらにサブクラスを追加することもできますが、acceptを実装し、visit...メソッドのいずれかを呼び出す必要があるため、ConsまたはNil

20
Petr Pudlák

C#の名前付きパラメーター(C#4.0で導入)を悪用した場合、次の条件に一致しやすい代数的データ型を作成できます。

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

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

C#では、そのEmpty型を使用できません。具体化により、メンバー型ごとに基本型が異なるためです。 Empty<T>;それほど役に立たない。

Javaでは、Empty : ConsList型消去のためですが、型チェッカーがどこかで叫ばないかどうかはわかりません。

ただし、どちらの言語にもnullがあるので、-allの参照タイプは「Whatever | Null」であると考えることができます。したがって、nullを "Empty"として使用するだけで、派生元を指定する必要がなくなります。

5
Jan Hudec

単純なサブクラス化以外に必要なことは、クラスをシールする方法、つまりサブクラスを階層に追加できないようにする方法だけです。

Javaではできません。ただし、基本クラスをパッケージプライベートとして宣言できます。つまり、すべての直接サブクラスは基本クラスと同じパッケージに属している必要があります。次に、最終的にサブクラスになるため、これ以上サブクラス化することはできません。

これがあなたの実際の問題に対処するかどうかはわかりません...

3
Stephen C

データ型ConsList<A>は、インターフェースとして表すことができます。インターフェイスは単一のdeconstructメソッドを公開します。これにより、そのタイプの値を「分解」できます。つまり、可能な各コンストラクターを処理できます。 deconstructメソッドの呼び出しは、HaskellまたはMLのcase ofフォームに類似しています。

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

deconstructメソッドは、ADTの各コンストラクターの「コールバック」関数を取ります。私たちの場合、それは空のリストの場合の関数と、「コンスセル」の場合の別の関数を取ります。

各コールバック関数は、コンストラクターが受け入れる値を引数として受け入れます。したがって、「空のリスト」の場合は引数を取りませんが、「コンセル」の場合は2つの引数を取ります。リストの先頭と末尾です。

Tupleクラスを使用するか、カリー化を使用して、これらの「複数の引数」をエンコードできます。この例では、単純なPairクラスを使用することを選択しました。

インターフェイスは、コンストラクタごとに1回実装されます。まず、「空のリスト」の実装があります。 deconstruct実装は、単に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());
  }
}

次に、「コンセル」ケースを同様に実装します。今回は、クラスに空でないリストの先頭と末尾のプロパティがあります。 deconstruct実装では、これらのプロパティは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));
  }
}

このADTのエンコードの使用例を次に示します。通常の折り返しリストであるreduce関数を記述できます。

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

これは、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

単純なサブクラス化以外に必要なことは、クラスをシールする方法、つまりサブクラスを階層に追加できないようにする方法だけです。

C#やJavaなどの言語でこの問題にどのように取り組みますか?

これを行うための良い方法はありませんが、恐ろしいハックを受け入れたい場合は、抽象基本クラスのコンストラクターに明示的な型チェックを追加できます。 Javaでは、これは次のようなものになります

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

具現化されたジェネリックスのため、C#ではより複雑になります。最も単純なアプローチは、型を文字列に変換し、それを変換することです。

Javaでは、このメカニズムでさえも、シリアライゼーションモデルまたはSun.misc.Unsafe

2
Peter Taylor