it-swarm.com.ru

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

Есть ли способ определить тип суммы в Java? Похоже, что Java естественным образом поддерживает типы продуктов напрямую, и я подумал, что перечисления могут позволить ей поддерживать типы сумм, а наследование выглядит так, как будто это может быть сделано, но есть по крайней мере один случай, который я не могу разрешить ... тип sum - это тип, который может иметь ровно один из множества различных типов, например теговое объединение в C . В моем случае я пытаюсь реализовать тип Either в 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:

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

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

36
Zoey Hewll

Сделайте 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> для any type B.

49
gdejohn

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

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

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

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

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

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

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

1
Alexander