web-dev-qa-db-ja.com

Javaタグ付きユニオン/合計タイプ

Javaで合計タイプを定義する方法はありますか? Javaは自然に製品タイプを直接サポートしているようで、enumはsumタイプをサポートできると思い、継承は多分それができるように見えますが、少なくとも1つのケースがあります '詳述すると、合計型とは、Cのタグ付き共用体のように、異なる型のセットの1つを正確に持つことができる型のことです。

data Either a b = Left a | Right b

しかし、基本レベルでは、製品タイプとして実装する必要があり、そのフィールドの1つを無視するだけです。

public class Either<L,R>
{
    private L left = null;
    private R right = null;

    public static <L,R> Either<L,R> right(R right)
    {
        return new Either<>(null, right);
    }

    public static <L,R> Either<L,R> left(L left)
    {
        return new Either<>(left, null);
    }

    private Either(L left, R right) throws IllegalArgumentException
    {
        this.left = left;
        this.right = right;
        if (left != null && right != null)
        {
            throw new IllegalArgumentException("An Either cannot be created with two values");
        }
        if (left == right)
        {
            throw new IllegalArgumentException("An Either cannot be created without a value");
        }
    }

    .
    .
    .
}

私はこれを継承で実装しようとしましたが、ワイルドカード型のパラメータ、または同等のものを使用する必要があります。Javaジェネリックでは許可されません:

public class Left<L> extends Either<L,?>

私はJavaのEnumをあまり使用しませんでしたが、JavaのEnumが次善の候補のように見えますが、希望はありません。
この時点で、これはObject値を型キャストすることによってのみ可能になると思います。一度だけ、安全に、できる方法がない限り、これを完全に回避したいと思います。これをすべての合計タイプに使用します。

39
Zoey Hewll

Eitherを1つのプライベートコンストラクターを持つ抽象クラスにし、クラス内に「データコンストラクター」(leftおよびright静的ファクトリーメソッド)をネストして、プライベートコンストラクターを表示できるようにします。そうでなければ、型を効果的に封印することができます。

抽象メソッド either を使用して、完全なパターンマッチングをシミュレートし、静的ファクトリメソッドによって返される具象型を適切にオーバーライドします。便利なメソッドを実装します( fromLeftfromRightbimapfirstsecondeitherに関して。

import Java.util.Optional;
import Java.util.function.Function;

public abstract class Either<A, B> {
    private Either() {}

    public abstract <C> C either(Function<? super A, ? extends C> left,
                                 Function<? super B, ? extends C> right);

    public static <A, B> Either<A, B> left(A value) {
        return new Either<>() {
            @Override
            public <C> C either(Function<? super A, ? extends C> left,
                                Function<? super B, ? extends C> right) {
                return left.apply(value);
            }
        };
    }

    public static <A, B> Either<A, B> right(B value) {
        return new Either<>() {
            @Override
            public <C> C either(Function<? super A, ? extends C> left,
                                Function<? super B, ? extends C> right) {
                return right.apply(value);
            }
        };
    }

    public Optional<A> fromLeft() {
        return this.either(Optional::of, value -> Optional.empty());
    }

    // other convenience methods
}

快適で安全!それを台無しにする方法はありません。

class Left<L> extends Either<L,?>を実行しようとしていた問題については、署名<A, B> Either<A, B> left(A value)を考慮してください。型パラメーターBは、パラメーターリストに表示されません。したがって、何らかのタイプAの値を指定すると、anyタイプBに対してEither<A, B>を取得できます。

51
gdejohn

和型をエンコードする標準的な方法は、代数的データ型をそのeliminator、つまり、パターンマッチングを行う関数。 Haskellの場合:

left :: a -> (a -> r) -> (b -> r) -> r
left x l _ = l x

right :: b -> (a -> r) -> (b -> r) -> r
right x _ r = r x

match :: (a -> r) -> (b -> r) -> ((a -> r) -> (b -> r) -> r) -> r
match l r k = k l r

-- Or, with a type synonym for convenience:

type Either a b r = (a -> r) -> (b -> r) -> r

left :: a -> Either a b r
right :: b -> Either a b r
match :: (a -> r) -> (b -> r) -> Either a b r -> r

Javaこれは訪問者のように見えます:

public interface Either<A, B> {
    <R> R match(Function<A, R> left, Function<B, R> right);
}

public final class Left<A, B> implements Either<A, B> {

    private final A value;

    public Left(A value) {
        this.value = value;
    }

    public <R> R match(Function<A, R> left, Function<B, R> right) {
        return left.apply(value);
    }

}

public final class Right<A, B> implements Either<A, B> {

    private final B value;

    public Right(B value) {
        this.value = value;
    }

    public <R> R match(Function<A, R> left, Function<B, R> right) {
        return right.apply(value);
    }

}

使用例:

Either<Integer, String> result = new Left<Integer, String>(42);
String message = result.match(
  errorCode -> "Error: " + errorCode.toString(),
  successMessage -> successMessage);

便宜上、型パラメーターを毎回言及することなく、LeftおよびRight値を作成するためのファクトリーを作成できます。結果を生成せずにパターンマッチングのオプションが必要な場合は、Consumer<A> left, Consumer<B> rightの代わりにFunction<A, R> left, Function<B, R> rightを受け入れるmatchのバージョンを追加することもできます。

19
Jon Purdy

さて、継承ソリューションは間違いなく最も有望です。やりたいことはclass Left<L> extends Either<L, ?>、これは残念ながらJavaの一般的なルールのためにできません。ただし、LeftまたはRightのタイプが「代替」可能性をエンコードする必要があるという譲歩をすれば、これを行うことができます。

public class Left<L, R> extends Either<L, R>`

今、私たちはLeft<Integer, A>からLeft<Integer, B>、実際にはseの2番目のタイプのパラメーターではないため。内部でこの変換を行うメソッドを定義して、その自由を型システムにエンコードできます。

public <R1> Left<L, R1> phantom() {
  return new Left<L, R1>(contents);
}

完全な例:

public class EitherTest {

  public abstract static class Either<L, R> {}

  public static class Left<L, R> extends Either<L, R> {

    private L contents;

    public Left(L x) {
      contents = x;
    }

    public <R1> Left<L, R1> phantom() {
      return new Left<L, R1>(contents);
    }

  }

  public static class Right<L, R> extends Either<L, R> {

    private R contents;

    public Right(R x) {
      contents = x;
    }

    public <L1> Right<L1, R> phantom() {
      return new Right<L1, R>(contents);
    }

  }

}

もちろん、実際にコンテンツにアクセスし、値がLeftまたはRightであるかどうかを確認するための関数を追加して、instanceofを明示的に振りかける必要がないようにします。どこでもキャストしますが、少なくともこれで開始するには十分です。

6
Silvio Mayolo

継承canを使用して合計タイプ(非結合ユニオン)をエミュレートしますが、対処する必要があるいくつかの問題があります。

  1. 他の人があなたのタイプに新しいケースを追加しないように注意する必要があります。これは、発生する可能性のあるすべてのケースを徹底的に処理する場合に特に重要です。非最終的なスーパークラスとパッケージプライベートコンストラクタで可能です。
  2. パターンパッチの欠如により、このタイプの値を使用することは非常に困難になります。すべてのケースを徹底的に処理したことを保証するためにコンパイラーでチェックされた方法が必要な場合は、自分で一致関数を実装する必要があります。
  3. APIの2つのスタイルのいずれかに追い込まれますが、どちらも理想的ではありません:
    • すべてのケースは共通のAPIを実装し、サポートしていないAPIにエラーをスローします。 Optional.get() を検討してください。理想的には、このメソッドは、値がsomeではなくnoneであることがわかっている、互いに素なタイプでのみ使用できます。しかし、それを行う方法はないので、一般的なOptional型のインスタンスメンバーです。 「case」が「none」であるオプションで呼び出すと、NoSuchElementExceptionがスローされます。
    • 各ケースには独自のAPIがあり、何ができるかを正確に伝えますが、これらのサブクラス固有のメソッドの1つを呼び出すたびに手動での型チェックとキャストが必要です。
  4. 「ケース」を変更するには、新しいオブジェクトの割り当てが必要です(また、頻繁に行うとGCに負荷がかかります)。

TL; DR:Javaの関数型プログラミングは快適な体験ではありません。

1
Alexander