代数的データ型で簡単に解決できる問題がいくつかあります。たとえば、リスト型は次のように非常に簡潔に表現できます。
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つの障害は次のとおりです。
class Empty : ConsList< ??? >
に何を入力するかを理解できませんでした)ConsList
にサブクラスを階層に追加できないようにする方法を理解できませんでしたC#やJavaで代数的データ型を実装する最も慣用的な方法は何でしょうか?または、それが不可能な場合、慣用的な置き換えは何でしょうか?
簡単ですが、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
これは、パターンマッチングを補足する 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
。
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);
}
}
C#では、そのEmpty
型を使用できません。具体化により、メンバー型ごとに基本型が異なるためです。 Empty<T>
;それほど役に立たない。
Javaでは、Empty : ConsList
型消去のためですが、型チェッカーがどこかで叫ばないかどうかはわかりません。
ただし、どちらの言語にもnull
があるので、-allの参照タイプは「Whatever | Null」であると考えることができます。したがって、null
を "Empty"として使用するだけで、派生元を指定する必要がなくなります。
単純なサブクラス化以外に必要なことは、クラスをシールする方法、つまりサブクラスを階層に追加できないようにする方法だけです。
Javaではできません。ただし、基本クラスをパッケージプライベートとして宣言できます。つまり、すべての直接サブクラスは基本クラスと同じパッケージに属している必要があります。次に、最終的にサブクラスになるため、これ以上サブクラス化することはできません。
これがあなたの実際の問題に対処するかどうかはわかりません...
データ型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
単純なサブクラス化以外に必要なことは、クラスをシールする方法、つまりサブクラスを階層に追加できないようにする方法だけです。
C#やJavaなどの言語でこの問題にどのように取り組みますか?
これを行うための良い方法はありませんが、恐ろしいハックを受け入れたい場合は、抽象基本クラスのコンストラクターに明示的な型チェックを追加できます。 Javaでは、これは次のようなものになります
protected ConsList() {
Class<?> clazz = getClass();
if (clazz != Empty.class && clazz != ConsCell.class) throw new Exception();
}
具現化されたジェネリックスのため、C#ではより複雑になります。最も単純なアプローチは、型を文字列に変換し、それを変換することです。
Javaでは、このメカニズムでさえも、シリアライゼーションモデルまたはSun.misc.Unsafe
。