Czy istnieje odpowiednik Scala's Either w Javie 8?

81

Tak jak java.util.Optional<T>w Javie 8 jest (w pewnym stopniu) odpowiednikiem typu Scali Option[T], czy istnieje odpowiednik Scali Either[L, R]?

HRJ
źródło
Wciąż brak standardowej implementacji po tak długim czasie :(
Cherry,
Zawsze jest java.lang.Object ... smutno.
Alex R

Odpowiedzi:

68

Nie ma Eithertypu Java 8, więc musisz utworzyć ją samodzielnie lub skorzystać z biblioteki innej firmy.

Możesz zbudować taką funkcję za pomocą nowego Optionaltypu (ale przeczytaj do końca tej odpowiedzi):

final class Either<L,R>
{
    public static <L,R> Either<L,R> left(L value) {
        return new Either<>(Optional.of(value), Optional.empty());
    }
    public static <L,R> Either<L,R> right(R value) {
        return new Either<>(Optional.empty(), Optional.of(value));
    }
    private final Optional<L> left;
    private final Optional<R> right;
    private Either(Optional<L> l, Optional<R> r) {
      left=l;
      right=r;
    }
    public <T> T map(
        Function<? super L, ? extends T> lFunc,
        Function<? super R, ? extends T> rFunc)
    {
        return left.<T>map(lFunc).orElseGet(()->right.map(rFunc).get());
    }
    public <T> Either<T,R> mapLeft(Function<? super L, ? extends T> lFunc)
    {
        return new Either<>(left.map(lFunc),right);
    }
    public <T> Either<L,T> mapRight(Function<? super R, ? extends T> rFunc)
    {
        return new Either<>(left, right.map(rFunc));
    }
    public void apply(Consumer<? super L> lFunc, Consumer<? super R> rFunc)
    {
        left.ifPresent(lFunc);
        right.ifPresent(rFunc);
    }
}

Przykład użycia:

new Random().ints(20, 0, 2).mapToObj(i -> (Either<String,Integer>)(i==0?
  Either.left("left value (String)"):
  Either.right(42)))
.forEach(either->either.apply(
  left ->{ System.out.println("received left value: "+left.substring(11));},
  right->{ System.out.println("received right value: 0x"+Integer.toHexString(right));}
));

Z perspektywy czasu, Optionalrozwiązanie oparte bardziej przypomina akademicki przykład, ale nie jest zalecane. Jednym z problemów jest traktowanie nulljako „pustego”, co jest sprzeczne ze znaczeniem „albo”.

Poniższy kod przedstawia kod Eitheruwzględniający nullmożliwą wartość, więc jest to ściśle „albo”, lewy lub prawy, nawet jeśli wartość to null:

abstract class Either<L,R>
{
    public static <L,R> Either<L,R> left(L value) {
        return new Either<L,R>() {
            @Override public <T> T map(Function<? super L, ? extends T> lFunc,
                                       Function<? super R, ? extends T> rFunc) {
                return lFunc.apply(value);
            }
        };
    }
    public static <L,R> Either<L,R> right(R value) {
        return new Either<L,R>() {
            @Override public <T> T map(Function<? super L, ? extends T> lFunc,
                                       Function<? super R, ? extends T> rFunc) {
                return rFunc.apply(value);
            }

        };
    }
    private Either() {}
    public abstract <T> T map(
      Function<? super L, ? extends T> lFunc, Function<? super R, ? extends T> rFunc);

    public <T> Either<T,R> mapLeft(Function<? super L, ? extends T> lFunc) {
        return this.<Either<T,R>>map(t -> left(lFunc.apply(t)), t -> (Either<T,R>)this);
    }
    public <T> Either<L,T> mapRight(Function<? super R, ? extends T> lFunc) {
        return this.<Either<L,T>>map(t -> (Either<L,T>)this, t -> right(lFunc.apply(t)));
    }
    public void apply(Consumer<? super L> lFunc, Consumer<? super R> rFunc) {
        map(consume(lFunc), consume(rFunc));
    }
    private <T> Function<T,Void> consume(Consumer<T> c) {
        return t -> { c.accept(t); return null; };
    }
}

Łatwo to zmienić na ścisłe odrzucenie null, po prostu wstawiając znak Objects.requireNonNull(value)na początku obu metod fabrycznych. Podobnie, można sobie wyobrazić dodanie obsługi pustego obu.

Holger
źródło
11
Pamiętaj, że chociaż zachowuje się tak Either, typ jest w pewnym sensie „za duży”, ponieważ pola lefti rightpola mogą w zasadzie być puste lub oba mogą być zdefiniowane. Ukryłeś konstruktory, które by to umożliwiały, ale to podejście nadal pozostawia potencjalne błędy w twojej implementacji. W typie prostych arytmetycznych warunkach, starasz się dostać a + bz (1 + a) * (1 + b). Jasne, a + bpojawia się w wyniku tego wyrażenia, ale tak jest 1i a * b.
Tajemniczy Dan
6
@Tajemniczy Dan: zakazanie określonego stanu podczas budowy obiektu jest preferowanym sposobem w Javie. W przeciwnym razie musiałbyś wymyślić nowy „prawidłowy zakres” dla prawie każdego przypadku użycia, intponieważ używanie całego zakresu wartości, intgdy użycie intzmiennej jest wyjątkiem, tak jak na przykładzie. W końcu Optionalrobi to samo, wymuszając niezmienniki podczas budowy obiektu.
Holger
1
@Holger: Either.left(42).map(left -> null, right -> right)rzuca NoSuchElementException(poprawne) na this.right.get()(niepoprawne). Można również ominąć wymuszanie i wytwarzanie niezmienników Either<empty, empty>do Either.left(42).mapLeft(left -> null). Lub po złożeniu, znowu nie Either.left(42).mapLeft(left -> null).map(left -> left, right -> right).
charlie
2
@charlie: to rozwiązanie nie uwzględnia Optional.mapmożliwości powrotu funkcji null, zmieniając ją w pustą Optional. Jednak poza możliwością wykrycia tego i natychmiastowego wyrzucenia nie widzę żadnego alternatywnego rozwiązania, które byłoby „bardziej poprawne”. Ahaik, nie ma zachowania referencyjnego, tak jak w Scali, nie możesz zmapować do null
Holger
1
@Holger: Zgadzam się, że nie ma bardziej poprawnego sposobu, po prostu nie podobał mi się fakt, że Rightścieżka kodu jest w ogóle wykonywana na Leftprzykład, mimo że błąd jest ten sam. I tak, wolałbym ponieść porażkę od razu, zamiast otrzymać <empty, empty>i oblać później. Ale znowu wszystko jest kwestią gustu / stylu.
charlie
26

W chwili pisania tego tekstu vavr (dawniej javaslang) jest prawdopodobnie najpopularniejszą funkcjonalną biblioteką Java 8. Jest całkiem podobny do lambda-companion albo w mojej drugiej odpowiedzi.

Either<String,Integer> value = compute().right().map(i -> i * 2).toEither();
wir
źródło
17

W bibliotece Java Standard Library nie ma żadnego żadnego. Jednak istnieje implementacja Either w FunctionalJava , wraz z wieloma innymi ładnymi klasami.

Ravi Kiran
źródło
Wydaje się, że link do jednego z nich został usunięty. Czy na pewno projekt jest nadal obsługiwany?
Flame_Phoenix,
Zaktualizowałem link. Projekt jest nadal aktywnie utrzymywany AFAIK.
Ravi Kiran
Tak, ale dokumentacja jest fatalna. :(
Misha Tavkhelidze
10

Cyklop reagują ma „prawo” tendencyjne albo realizację nazwie Xor .

 Xor.primary("hello")
    .map(s->s+" world")

 //Primary["hello world"]

 Xor.secondary("hello")
    .map(s->s+" world")

 //Secondary["hello"]

 Xor.secondary("hello")
    .swap()
    .map(s->s+" world")

 //Primary["hello world"]

Xor.accumulateSecondary(ListX.of(Xor.secondary("failed1"),
                                 Xor.secondary("failed2"),
                                 Xor.primary("success")),
                                 Semigroups.stringConcat)

//failed1failed2

Istnieje również pokrewny typ Ior, który może działać jako albo albo krotka2.

  • ujawnienie Jestem autorem cyklop-reaguj.
John McClean
źródło
6

Nie, nie ma.

Programiści języka Java wyraźnie stwierdzają, że typy takie jak Option<T>mają być używane tylko jako wartości tymczasowe (np. W wynikach operacji strumieniowych), więc chociaż tym samym, co w innych językach, nie powinny być używane tak, jak są używane w innych Języki. Nic więc dziwnego, że nie ma czegoś takiego jakEither to, że nie powstaje naturalnie (np. Z operacji strumieniowych) tak jak Optionalma.

Vladimir Matveev
źródło
8
Czy masz na to źródło?
Cannoliopsida,
3
@akroy, to wydaje się być poprawne, Brian Goetz napisał to samo w tej odpowiedzi: link .
RonyHe
2
U mnie Eitherpojawia się naturalnie. Może robię to źle. Co robisz, gdy metoda może zwrócić dwie różne rzeczy? Podobnie jak Either<List<String>, SomeOtherClass>?
tamas.kenez
3
Sprzeciwiłbym się również temu, że Albo pojawia się naturalnie, dla mnie w strumieniu, w którym operacja mapy mogłaby potencjalnie zgłosić wyjątek, więc mapuję na strumień albo <Wyjątek, Wynik>
Olivier Gérardin
6

W Eithermałej bibliotece istnieje samodzielna implementacja „ambiwalencji”: http://github.com/poetix/ambivalence

Możesz go zdobyć z centrali Maven:

<dependency>
    <groupId>com.codepoetics</groupId>
    <artifactId>ambivalence</artifactId>
    <version>0.2</version>
</dependency>
Dominic Fox
źródło
5

lambda-companion ma Eithertyp (i kilka innych funkcjonalnych typów np. Try)

<dependency>
    <groupId>no.finn.lambda</groupId>
    <artifactId>lambda-companion</artifactId>
    <version>0.25</version>
</dependency>

Korzystanie z niego jest łatwe:

final String myValue = Either.right("example").fold(failure -> handleFailure(failure), Function.identity())
wir
źródło
1
Projekt nie jest już w trakcie konserwacji.
Flame_Phoenix,