Подтвердить что ты не робот

Java-маркированный тип объединения/суммы

Есть ли способ определить тип суммы в Java? Кажется, что Java, естественно, поддерживает типы продуктов напрямую, и я думал, что перечисления могут позволить ему поддерживать типы сумм, а наследование выглядит, может быть, оно может это сделать, но есть хотя бы один случай, который я не могу решить. Чтобы разработать, тип суммы - это тип, который может иметь ровно один из множества различных типов, например, тегированный союз в C. В моем случае я пытаюсь реализовать haskell. Любой тип в Java:

data Either a b = Left a | Right b

но на базовом уровне мне нужно реализовать его как тип продукта и просто игнорировать одно из его полей:

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 generics:

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

Я много не использовал Java Enums, но пока они кажутся следующим лучшим кандидатом, я не надеюсь.
На данный момент, я думаю, что это может быть возможно только по типу литья Object значений, которые я хотел бы надеяться, чтобы полностью избежать, если есть способ, чтобы сделать это один раз, безопасно, и быть в состоянии использовать это для всех типов сумм.

4b9b3361

Ответ 1

Сделайте Either абстрактный класс с одним, частным конструктором, либо вставьте его "конструкторы данных" (left и right статические методы фабрики) внутри класса, чтобы они могли видеть частный конструктор, но ничто другое не может, эффективно запечатывая тип.

Используйте абстрактный метод either для имитации исчерпывающего сопоставления с образцом, перекрывая надлежащим образом в конкретных типах возвращаемых статических методов фабрики. Внедрите удобные методы (например, fromLeft, fromRight, bimap, first, second) с точки зрения either.

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, вы можете получить Either<A, B> для любого типа B

Ответ 2

Стандартный способ кодирования типов сумм - это кодирование Boehm-Berarducci (часто называемое именем его двоюродного брата, церковного кодирования), которое представляет собой алгебраический тип данных в качестве его выпрямителя, то есть функцию, которая выполняет сопоставление с образцом:

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

В 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 без необходимости указывать параметры типа каждый раз; вы также можете добавить версию match которая будет принимать Consumer<A> left, Consumer<B> right вместо Function<A, R> left, Function<B, R> right если вы хотите, чтобы опция сопоставления шаблонов не создавала результат.

Ответ 3

Хорошо, поэтому решение наследования, безусловно, является наиболее перспективным. То, что мы хотели бы сделать, это class Left<L> extends Either<L,?>, Который, к сожалению, мы не можем сделать из-за общих правил Java. Однако, если мы сделаем уступки, что тип Left или Right должен кодировать "альтернативную" возможность, мы можем это сделать.

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

Теперь мы хотели бы иметь возможность конвертировать Left<Integer, A> в Left<Integer, B>, поскольку на самом деле он не использует этот параметр второго типа. Мы можем определить метод, чтобы сделать это преобразование внутренне, таким образом, кодируя эту свободу в систему типов.

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 и явные приведения повсюду, но этого должно быть достаточно для начала работы, по крайней мере.

Ответ 4

Наследование может использоваться для эмуляции типов сумм (Disjoint union), но есть несколько проблем, с которыми вам нужно иметь дело:

  1. Вам нужно позаботиться о том, чтобы другие не добавляли новые случаи в ваш тип. Это особенно важно, если вы хотите исчерпывающе обрабатывать каждый случай, который может возникнуть. Это возможно с помощью нефинального суперкласса и конструктора private-package-private.
  2. Отсутствие патча шаблонов затрудняет потребление такого значения. Если вы хотите, чтобы проверенный компилятором способ гарантировать, что вы исчерпывающе обрабатывали все случаи, вам нужно реализовать функцию соответствия самостоятельно.
  3. Вы попали в один из двух типов API, ни один из которых не идеален:
    • Все случаи реализуют общий API, бросая ошибки в API, которые они не поддерживают. Рассмотрим Optional.get(). В идеале, этот метод будет доступен только на дизъюнктный типе, которые ценят, как известно, some, а не none. Но нет способа сделать это, поэтому он является членом экземпляра общего Optional типа. Он выдает NoSuchElementException если вы вызываете его на факультативном, чей "случай" - "none".
    • Каждый случай имеет уникальный API, который сообщает вам точно, на что он способен, но для этого требуется ручная проверка типа и литье каждый раз, когда вы хотите вызвать один из этих подклассов.
  4. Для изменения "случаев" требуется новое распределение объектов (и, если это делается, добавляется давление на GC).

TL; DR: Функциональное программирование на Java не является приятным опытом.