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


58

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

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

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

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

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

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



3
C#はOOP言語です。OOPを使用して問題を解決します。他のパラダイムを使用しないでください。
陶酔

7
@Euphoric C#は、C#3.0で非常に使いやすい関数型言語になりました。一流の関数、組み込みの一般的な関数演算、モナド。
マウリシオシェファー

2
@Euphoric:一部のドメインはオブジェクトを使用して簡単にモデル化でき、代数データ型を使用してモデル化するのは困難ですが、一部は逆です。両方の方法を知っていると、ドメインをより柔軟にモデリングできます。そして、私が言ったように、代数的データ型を典型的なオブジェクト指向の概念にマッピングすることはそれほど複雑ではありません:データ型は抽象基本クラス(またはインターフェース、または抽象特性)になり、データコンストラクターは具体的な実装サブクラスになります。これにより、オープンな代数データ型が得られます。継承の制限により、閉じた代数データ型が提供されます。多態性は、あなたにケース差別を与えます。
ヨルグWミットタグ

3
@ユーフォリック、パラダイム、シュマラディグム、誰が気にしますか?ADTは、関数型プログラミング(またはOOPなど)に直交しています。あらゆる言語のASTのエンコードは、適切なADTのサポートなしでは非常に苦痛であり、その言語のコンパイルは、別のパラダイムに依存しない機能であるパターンマッチングなしでは苦痛です。
SKロジック

回答:


42

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


2
どういうわけか、ここにあなたの名前が表示されるのを見て驚くことはありません!ありがとう、私はこのイディオムを知らなかった。
ヨルグWミットタグ

4
あなたが「ボイラープレートが重い」と言ったとき、私はもっとひどいことに備えました;-) Javaはボイラープレートでかなり悪いことがあります。
ヨアヒムザウアー

しかし、これは構成しません:キャストを介してそれを主張することなくタイプAを特殊化する方法はありません(私は思う)
ニコラス14

残念ながら、これはいくつかのより複雑な合計タイプを表すことができないようEitherです。私の質問を
ゾーイヒュール

20

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

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つを分解する方法を宣言します。さらにサブクラスを追加できますがacceptvisit...メソッドの1つを呼び出して実装する必要があるため、like Consまたはlikeのいずれかを実行する必要がありNilます。


13

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

以前にこの手法のJavaバージョンを見たことがありますが、ラムダと名前付きパラメーターを使用すると非常に読みやすくなります。+1!
ドーバル14

1
ここでの問題は、Rightはエラーの種類に対して一般的ではないということだと思います。次のようなものですclass Right<R> : Either<Bot,R>。ここで、Eitherは共変(out)型パラメーターを持つインターフェイスに変更され、Botは下部型(他のすべての型のサブタイプ、Objectの反対側)です。C#にはボトム型があるとは思わない。
クロイド

5

C#では、具体Empty化のために、ベースタイプはメンバータイプごとに異なるため、そのタイプを持つことはできません。あなただけを持つことができますEmpty<T>。それほど便利ではありません。

Javaでは、Empty : ConsList型消去が原因で発生する可能性がありますが、型チェッカーがどこかで悲鳴を上げないかどうかはわかりません。

ただし、両方の言語にがあるためnullすべての参照タイプを「Whatever | Null」と考えることができます。そのnullため、「空」としてを使用するだけで、派生するものを指定する必要がなくなります。


問題nullは、それがあまりにも一般的であるということです:それは何も存在しないこと、すなわち一般にであることを表しますが、リスト要素、すなわち特に空のリストがないことを表現したいです。空のリストと空のツリーには異なるタイプが必要です。また、空のリストは独自の動作を保持するため、実際の値である必要があり、独自のメソッドが必要です。リストを作成するために(または右結合演算子を使用する言語で)[1, 2, 3]言いたいのですが、言うことはできません。Empty.prepend(3).prepend(2).prepend(1)1 :: 2 :: 3 :: Emptynull.prepend …
ヨルグWミットタグ

@JörgWMittag:ヌルには明確なタイプがあります。目的のために、値がnullの型付き定数を簡単に作成することもできます。ただし、メソッドを呼び出せないのは事実です。メソッドを使用したアプローチは、とにかく要素タイプ固有の空がないと機能しません。
ジャン・ヒューデック

いくつかの巧妙な拡張メソッドは、nullに対する「メソッド」呼び出しを偽造できます(もちろん、すべてが本当に静的です)
jk。

あなたは持つことができるEmptyEmpty<>、あなたがしたい場合は、かなり現実的なシミュレーションを可能にするために、暗黙の型変換演算子を虐待します。基本的に、Emptyコードで使用しますが、すべての型シグネチャなどは汎用バリアントのみを使用します。
イーモンネルボンヌ

3

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

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

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


本当の問題はありません。または、ここではなくStackOverflowに投稿しました:-)代数的データ型の重要な特性は、それらを閉じることができることです。つまり、ケースの数が固定されています。 、リストは空または空ではありません。これが静的であることを静的に保証できる場合、動的キャストまたは動的intanceofチェックを「疑似型安全」(つまり、コンパイラがそうでなくても安全であることがわかります)、これらの2つのケースを確認してください。ただし、他の誰かが新しいサブクラスを追加すると、予期しないランタイムエラーが発生する可能性があります。
ヨルグWミットタグ

@JörgWMittag-まあ、Javaは明らかにそれをサポートしていません...あなたが望んでいるように見えるという強い意味で。もちろん、実行時に不要なサブタイピングをブロックするためにさまざまなことを実行できますが、「予期しない実行時エラー」が発生します。
スティーブンC

3

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

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

このdeconstructメソッドは、ADTの各コンストラクターに対して「コールバック」関数を取ります。私たちの場合、空のリストの場合には関数を、「cons cell」の場合には別の関数を取ります。

各コールバック関数は、コンストラクターによって受け入れられる値を引数として受け入れます。したがって、「空のリスト」の場合は引数を取りませんが、「コンセル」の場合はリストの先頭と末尾の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());
  }
}

次に、「cons cell」ケースを同様に実装します。今回はクラスにプロパティがあります:空でないリストの先頭と末尾。では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

興味深いアプローチ、とてもいいです!F#Active PatternsとScala Extractorsへの接続を見ることができます(そして、おそらくHaskellビューへのリンクもありますが、残念ながら何も知りません)。パターンマッチングの責任をデータコンストラクターでADTインスタンス自体に移すことは考えていませんでした。
ヨルグWミッター

2

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

C#やJavaなどの言語でこの問題にどのように対処しますか?

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

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

C#では、ジェネリックが具体化されているため、より複雑です。最も単純なアプローチは、型を文字列に変換してマングルすることです。

Javaでは、このメカニズムでさえ、シリアル化モデルまたはを介して本当にやりたい人が理論的にバイパスできることに注意してくださいsun.misc.Unsafe


1
C#では、それほど複雑ではありませんType type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();
。– svick

@svick、よく観察された。基本型がパラメーター化されることを考慮していませんでした。
ピーターテイラー

ブリリアント!「手動の静的型チェック」を行うにはこれで十分だと思います。悪意ではなく、正直なプログラミングエラーを排除したいと考えています。
ヨルグWミットタグ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.