Tipos de unión / suma etiquetados en Java


¿Hay alguna manera de definir un tipo de suma en Java? Java parece apoyar naturalmente los tipos de productos directamente, y pensé que las enumeraciones podrían permitir que sea compatible con los tipos de suma, y la herencia parece que tal vez podría hacerlo, pero hay al menos un caso que no puedo resolver. Para elaborar, un tipo de suma es un tipo que puede tener exactamente uno de un conjunto de tipos diferentes, como una unión etiquetada en C. En mi caso, estoy tratando de implementar los dos tipos de haskell en Java:

data Either a b = Left a | Right b

Pero en el nivel de base estoy tener que implementarlo como un tipo de producto, y simplemente ignorar uno de sus campos:

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

    .
    .
    .
}

Intenté implementar esto con herencia, pero tengo que usar un parámetro de tipo comodín, o equivalente, que Java generics no permitirá:

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

No he utilizado mucho Enums de Java, pero si bien parecen el próximo mejor candidato, no tengo esperanzas.
En este punto, creo que esto solo podría ser posible mediante valores de tipo Object, que espero evitar por completo, a menos que haya una manera de hacerlo es una vez, con seguridad, y ser capaz de utilizar que para todos los tipos de suma.

Author: Zoey Hewll, 2018-01-08

4 answers

Haga Either una clase abstracta con un constructor privado y anide sus "constructores de datos" (left y right métodos de fábrica estáticos) dentro de la clase para que puedan ver el constructor privado pero nada más puede, sellando efectivamente el tipo.

Utilice un método abstractoeither para simular la coincidencia exhaustiva de patrones, sobrescribiendo apropiadamente en los tipos de concreto devueltos por los métodos de fábrica estáticos. Implementar métodos de conveniencia (como fromLeft, fromRight, bimap, first, second) en términos de 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
}

Agradable y seguro! No hay forma de arruinarlo.

En cuanto al problema que tenía tratando de hacer class Left<L> extends Either<L,?>, considere la firma <A, B> Either<A, B> left(A value). El parámetro type B no aparece en la lista de parámetros. Por lo tanto, dado un valor de algún tipo A, puede obtener un Either<A, B> para cualquier tipo B.

 47
Author: gdejohn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-21 03:56:31

Una forma estándar de codificación de tipos de suma es la codificación Boehm–Berarducci (a menudo referida por el nombre de su primo, Church encoding) que representa un tipo de datos algebraico como su eliminador , es decir, una función que hace coincidencia de patrones:

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

En Java esto se vería como un visitante:

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

}

Ejemplo de uso:

Either<Integer, String> result = new Left<Integer, String>(42);
String message = result.match(
  errorCode -> "Error: " + errorCode.toString(),
  successMessage -> successMessage);

Para mayor comodidad, puede crear una fábrica para crear valores Left y Right sin tener que mencionar los parámetros de tipo cada vez; también puede agregar una versión de match que acepte Consumer<A> left, Consumer<B> right en lugar de Function<A, R> left, Function<B, R> right si desea la opción de coincidencia de patrones sin producir un resultado.

 18
Author: Jon Purdy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-08 02:25:38

Bien, por lo que la solución de herencia es sin duda la más prometedora. Lo que nos gustaría hacer es class Left<L> extends Either<L, ?>, lo que desafortunadamente no podemos hacer debido a las reglas genéricas de Java. Sin embargo, si hacemos las concesiones de que el tipo de Left o Right debe codificar la posibilidad "alternativa", podemos hacer esto.

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

Ahora, nos gustaría poder convertir Left<Integer, A> a Left<Integer, B>, ya que en realidad no usa ese segundo parámetro de tipo. Podemos definir un método para hacer esto conversión interna, codificando así esa libertad en el sistema de tipos.

public <R1> Left<L, R1> phantom() {
  return new Left<L, R1>(contents);
}

Ejemplo completo:

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

  }

}

Por supuesto, querrá agregar algunas funciones para acceder realmente al contenido, y para verificar si un valor es Left o Right para que no tenga que rociar instanceof y moldes explícitos en todas partes, pero esto debería ser suficiente para comenzar, como mínimo.

 6
Author: Silvio Mayolo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-08 02:03:00

La herencia se puede usar para emular tipos de suma (uniones disjuntas), pero hay algunos problemas con los que debe lidiar:

  1. Debe tener cuidado para evitar que otros agreguen nuevos casos a su tipo. Esto es especialmente importante si desea manejar exhaustivamente todos los casos que pueda encontrar. Es posible con una superclase no final y un constructor package-private.
  2. La falta de parches de patrones hace que sea bastante difícil consumir un valor de este tipo. Si si desea verificar el compilador para garantizar que ha manejado todos los casos de manera exhaustiva, debe implementar una función de coincidencia usted mismo.
  3. Estás forzado a uno de los dos estilos de API, ninguno de los cuales es ideal:
    • Todos los casos implementan una API común, lanzando errores en la API que no admiten por sí mismos. Considere Optional.get(). Idealmente, este método solo estaría disponible en un tipo disjunto cuyo valor se sabe que es some en lugar de none. Pero no hay manera de hacer eso, así que es un miembro de instancia de un tipo general Optional. Lanza NoSuchElementException si lo llamas en un opcional cuyo "caso" es "ninguno".
    • Cada caso tiene una API única que le dice exactamente de qué es capaz, pero eso requiere una comprobación de tipo manual y un cast cada vez que desee llamar a uno de estos métodos específicos de subclase.
  4. Cambiar "casos" requiere una nueva asignación de objetos (y añade presión sobre el GC si se hace a menudo).

TL; DR: La programación funcional en Java es no es una experiencia agradable.

 1
Author: Alexander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-09 02:41:06