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.
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
.
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.
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.
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:
- 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.
- 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.
- 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 essome
en lugar denone
. Pero no hay manera de hacer eso, asà que es un miembro de instancia de un tipo generalOptional
. LanzaNoSuchElementException
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.
- Todos los casos implementan una API común, lanzando errores en la API que no admiten por sà mismos. Considere
- 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.
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