¿Existe un equivalente de Scala's Either en Java 8?

81

Al igual que java.util.Optional<T>en Java 8 es (algo) equivalente al Option[T]tipo de Scala , ¿hay un equivalente al de Scala Either[L, R]?

HRJ
fuente
Todavía no hay implementación estándar después de todo este tiempo :(
Cherry
Siempre hay java.lang.Object ... triste.
Alex R

Respuestas:

68

No hay ningún Eithertipo que sea Java 8, por lo que debe crear uno usted mismo o usar alguna biblioteca de terceros.

Puede crear una característica de este Optionaltipo utilizando el nuevo tipo (pero lea hasta el final de esta respuesta):

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);
    }
}

Ejemplo de caso de uso:

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));}
));

En retrospectiva, la Optionalsolución basada es más un ejemplo académico, pero no un enfoque recomendado. Un problema es el tratamiento de null"vacío" que contradice el significado de "cualquiera".

El siguiente código muestra un Eitherque considera nullun valor posible, por lo que es estrictamente "o", izquierda o derecha, incluso si el valor es 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; };
    }
}

Es fácil cambiar eso a un rechazo estricto nullsimplemente insertando un Objects.requireNonNull(value)al principio de ambos métodos de fábrica. Del mismo modo, sería imaginable agregar soporte para uno vacío.

Holger
fuente
11
Tenga en cuenta que si bien esto se comporta así Either, el tipo es en cierto sentido "demasiado grande", ya que sus campos lefty rightpodrían, en principio, estar ambos vacíos o ambos definidos. Ha ocultado los constructores que lo harían posible, pero el enfoque aún deja potencial para errores en su implementación. En términos aritméticos de tipo simple, estás tratando de a + bsalir (1 + a) * (1 + b). Claro, a + bocurre en el resultado de esa expresión, pero también lo es 1y a * b.
Dan misterioso
6
@Mysterious Dan: prohibir cierto tipo de estado durante la construcción de objetos es la forma preferida en Java. De lo contrario, tendría que inventar un nuevo tipo de "rango válido" para casi todos los casos de intuso, ya que usar todo el rango de valores intcuando se usa una intvariable es la excepción, solo como un ejemplo. Después de todo, Optionalhace lo mismo, aplicando las invariantes durante la construcción del objeto.
Holger
1
@Holger: Either.left(42).map(left -> null, right -> right)lanza NoSuchElementException(correcto) en this.right.get()(incorrecto). Además, se puede evitar la aplicación de invariantes y producir Either<empty, empty>por Either.left(42).mapLeft(left -> null). O cuando se junta, vuelve a fallar Either.left(42).mapLeft(left -> null).map(left -> left, right -> right).
charlie
2
@charlie: esta solución no considera que Optional.mappermita que la función regrese null, volviéndola vacía Optional. Sin embargo, además de la oportunidad de detectarlo y lanzarlo de inmediato, no veo ninguna solución alternativa que sea “más correcta”. Afaik, no hay un comportamiento de referencia, como en Scala, no se puede asignar a null
Holger
1
@Holger: Estoy de acuerdo en que no hay una forma "más correcta", simplemente no me gustó el hecho de que la Rightruta del código se ejecute en absoluto para una Leftinstancia, aunque el error es el mismo. Y sí, preferiría fracasar de inmediato en lugar de recibir una <empty, empty>y fracasar más tarde. Pero de nuevo, todo es solo una cuestión de gusto / estilo.
charlie
26

En el momento de escribir este artículo, vavr (anteriormente javaslang) es probablemente la biblioteca funcional de Java 8 más popular. Es bastante similar a Either de lambda-companion en mi otra respuesta.

Either<String,Integer> value = compute().right().map(i -> i * 2).toEither();
torbellino
fuente
17

No hay ninguno en la biblioteca estándar de Java. Sin embargo, hay una implementación de Either en FunctionalJava , junto con muchas otras clases interesantes.

Ravi Kiran
fuente
El enlace a Either parece haber sido eliminado. ¿Estás seguro de que el proyecto aún recibe apoyo?
Flame_Phoenix
He actualizado el enlace. El proyecto todavía se mantiene activamente AFAIK.
Ravi Kiran
Lo es, pero la documentación es pésima. :(
Misha Tavkhelidze
10

cyclops-react tiene una implementación sesgada 'a la derecha' llamada 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

También hay un tipo Ior relacionado que puede actuar como una o una tupla2.

  • divulgación Soy el autor de cyclops-react.
John McClean
fuente
6

No, no hay ninguno.

Los desarrolladores del lenguaje Java afirman explícitamente que los tipos como Option<T>están destinados a usarse solo como valores temporales (por ejemplo, en los resultados de las operaciones de flujo), por lo que si bien son lo mismo que en otros lenguajes, se supone que no deben usarse como se usan en otros Idiomas Por lo tanto, no es sorprendente que no exista Eitherporque no surge de forma natural (por ejemplo, de operaciones de flujo) como lo Optionalhace.

Vladimir Matveev
fuente
8
¿Tiene una fuente sobre eso?
Cannoliopsida
3
@akroy, esto parece ser correcto, Brian Goetz escribió tanto en esta respuesta: enlace .
RonyHe
2
Para mí Eithersurge naturalmente. Quizás lo estoy haciendo mal. ¿Qué haces cuando un método puede devolver dos cosas diferentes? Como Either<List<String>, SomeOtherClass>?
tamas.kenez
3
También objetaría que Either surge naturalmente, para mí en una secuencia donde una operación de mapa podría generar una excepción, por lo que mapeo a una secuencia de Either <Exception, Result>
Olivier Gérardin
6

Hay una implementación independiente de Eitheren una pequeña biblioteca, "ambivalencia": http://github.com/poetix/ambivalence

Puedes conseguirlo en Maven central:

<dependency>
    <groupId>com.codepoetics</groupId>
    <artifactId>ambivalence</artifactId>
    <version>0.2</version>
</dependency>
Dominic Fox
fuente
5

lambda-companion tiene un Eithertipo (y algunos otros tipos funcionales, por ejemplo Try)

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

Usarlo es fácil:

final String myValue = Either.right("example").fold(failure -> handleFailure(failure), Function.identity())
torbellino
fuente
1
El proyecto ya no está en mantenimiento.
Flame_Phoenix