Можно ли построить Either в Java, который позволяет flatMap возвращать другое значение Left? - PullRequest
0 голосов
/ 08 июня 2018

Я пытаюсь понять, как реализован Either.Я застрял при объединении нескольких функций таким образом, что можно возвращать другое значение Left во время flatMap.Я не могу понять, как это возможно в системе типов.

Минимальный Пример кода:

public class Either<A,B> {
    public final A left;
    public final B right;

    private Either(A a, B b) {
        left = a;
        right = b;
    }

    public static <A, B> Either<A, B> left(A a) {
        return new Either<>(a, null);
    }


    public static <A, B> Either<A, B> right(B b) {
        return new Either<>(null, b);
    }


    public <C> Either<A, C> flatMap(Function<B, Either<A,C>> f) {
        if (this.isRight()) return f.apply(this.right);
        else return Either.left(this.left);
    }

    // map and other useful functions....

Сначала я думал, что смогу отобразить разные значения Left, которые позволят возвращать соответствующую ошибку в каждой точке.

Так, например, учитывая эти функции:

public static Either<Foo, String> doThing() {
        return Either.right("foo");
}

public static Either<Bar, String> doThing2(String text) {
    return (text.equals("foo")) 
        ? Either.right("Yay!") 
        : Either.left(new Bar("Grr..."));
}

public static Either<Baz, String> doThing3() {
    return (text.equals("Yay!")) 
        ? Either.right("Hooray!") 
        : Either.left(new Baz("Oh no!!"));
}

Я думал, что смогу сделать

doThing().flatMap(x -> doThing2()).flatMap(y -> doThing3())

Однако компилятор помечает это как невозможное,

После некоторого изучения кода я понял, что это из-за моих <A,B> универсальных параметров.

flatMap имеет два разных случая:

  1. случай, когда мы отображаем правую сторону
  2. случай, когда мы пропускаем левое значение

Итак, если моя цель - разрешить иногда возвращать разные значения Left из flatMap, то две мои общие переменные <A,B> не работают, потому что если выполняется случай 1, а моя функция изменяется A, то случай 2 недопустимпотому что A! = A'.Акт применения функции к правой стороне, возможно, изменил левую сторону на другой тип.

Все это подводит меня к следующим вопросам:

  1. Является ли мое ожидание поведения типа Either неверным?
  2. Можно ли возвращать различные типы Left во время операции flatMap?
  3. если так, как вы заставляете типы работать?

Ответы [ 3 ]

0 голосов
/ 08 июня 2018

Можно, но ваш старый Left должен быть подтипом или равен новому Left, чтобы его можно было использовать.Я не очень знаком с синтаксисом Java, но реализация Scala выглядит следующим образом:

def flatMap[A1 >: A, B1](f: B => Either[A1, B1]): Either[A1, B1] = this match {
  case Right(b) => f(b)
  case _        => this.asInstanceOf[Either[A1, B1]]
}

Здесь A1 >: A обозначает A как подтип A1.Я знаю, что у Java есть синтаксис <A extends A1>, но я не уверен, что его можно использовать для описания ограничения на A1, как нам нужно в этом случае.

0 голосов
/ 22 июня 2018

Что касается использования Either (doThing(...)), то, похоже, вашего плоского отображения недостаточно.Я предполагаю, что вы хотите, чтобы ваше плоское отображение работало так же, как для Optional<T>.

Картограф Optional.flatMap принимает значение kind из T и возвращает Optional<U>, где U является параметром универсального типа этого метода.Но Optional имеет один параметр общего типа T, тогда как Either имеет два: A и B.Так что, если вы хотите отобразить карту Either<A,B> either, недостаточно использовать одно отображение.

Одно отображение, что должно отображаться?«Значение, которое не null», вы бы сказали, не так ли?Хорошо, но вы знаете это сначала во время выполнения.Ваш flatMap метод определяется во время компиляции.Поэтому вы должны предоставить сопоставление для каждого случая.

Вы выбираете <C> Either<A, C> flatMap(Function<B, Either<A, C>> f).Это отображение использует значение типа B в качестве ввода.Это означает, что если сопоставленный Either either равен !either.isRight(), все последующие сопоставления вернут Either.left(a), где a - это значение самого первого Either.left(a).Так что на самом деле только Either either, где either.isRight() может быть сопоставлено с другим значением.И это должно быть either.isRight() с самого начала.Это также означает, что после создания Either<A,B> either все плоские отображения приведут к виду из Either<A,?>.Таким образом, текущий flatMap ограничивает Either either сохранением своего левого универсального типа.Это то, что вы должны делать?

Если вы хотите создать плоскую карту Either either без ограничений, вам нужны отображения для обоих случаев: either.isRight() и !either.isRight().Это позволит вам продолжить плоское отображение в обоих направлениях.

Я сделал это следующим образом:

public class Either<A, B> {
    public final A left;
    public final B right;

    private Either(A a, B b) {
        left = a;
        right = b;
    }

    public boolean isRight() {
        return right != null;
    }

    @Override
    public String toString() {
        return isRight() ?
                right.toString() :
                left.toString();
    }

    public static <A, B> Either<A, B> left(A a) {
        return new Either<>(a, null);
    }

    public static <A, B> Either<A, B> right(B b) {
        return new Either<>(null, b);
    }

    public <C, D> Either<C, D> flatMap(Function<A, Either<C, D>> toLeft, Function<B, Either<C, D>> toRight) {
        if (this.isRight()) {
            return toRight.apply(this.right);
        } else {
            return toLeft.apply(this.left);
        }
    }

    public static void main(String[] args) {
        Either<String, String> left = Either.left(new Foo("left"))
                .flatMap(l -> Either.right(new Bar(l.toString() + ".right")), r -> Either.left(new Baz(r.toString() + ".left")))
                .flatMap(l -> Either.left(l.toString() + ".left"), r -> Either.right(r.toString() + ".right"));
        System.out.println(left); // left.right.right

        Either<String, String> right = Either.right(new Foo("right"))
                .flatMap(l -> Either.right(new Bar(l.toString() + ".right")), r -> Either.left(new Baz(r.toString() + ".left")))
                .flatMap(l -> Either.left(l.toString() + ".left"), r -> Either.right(r.toString() + ".right"))
                .flatMap(l -> Either.right(l.toString() + ".right"), r -> Either.left(r.toString() + ".left"));
        System.out.println(right); // right.left.left.right
    }

    private static class Foo {
        private String s;

        public Foo(String s) {
            this.s = s;
        }

        @Override
        public String toString() {
            return s;
        }
    }

    private static class Bar {
        private String s;

        public Bar(String s) {
            this.s = s;
        }

        @Override
        public String toString() {
            return s;
        }
    }

    private static class Baz {
        private String s;

        public Baz(String s) {
            this.s = s;
        }

        @Override
        public String toString() {
            return s;
        }
    }
}

Отвечая на ваш вопрос: Да, возможно построить Либо, возвращая другой левыйзначение.Но я думаю, что вы намеревались узнать, как получить правильную работу Either.

0 голосов
/ 08 июня 2018

Нет разумной функции flatMap(), как вы хотите, из-за параметричности.Подумайте:

Either<Foo, String> e1 = Either.left(new Foo());
Either<Bar, String> e2 = foo.flatMap(x -> doThing2());
Bar bar = e2.left; // Where did this come from???

flatMap() сама должна была бы как-то изобрести экземпляр Bar.Если вы начнете писать flatMap(), который может изменить оба типа, вы увидите проблему более четко:

public <C, D> Either<C, D> flatMap(Function<B, Either<C, D>> f) {
    if (this.isRight()) {
        return f.apply(this.right);
    } else {
        // Error: can't convert A to C
        return Either.left(this.left);
    }
}
...