sirve - settitle java
Tipos de unión/suma etiquetados Java (4)
Bien, entonces la solución de herencia definitivamente es la más prometedora. Lo que nos gustaría hacer es que la 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 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 esta conversión internamente, 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 a los contenidos, y para verificar si un valor es Left
o Right
para que no tenga que espolvorear instanceof
y moldes explícitos en todas partes, pero esto debería ser suficiente para comenzar. por lo menos.
¿Hay alguna forma de definir un tipo de suma en Java? Java parece respaldar naturalmente los tipos de productos directamente, y pensé que las enum podrían permitir que admitieran tipos de suma, y la herencia parece que 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 el tipo de Haskell en Java:
data Either a b = Left a | Right b
pero en el nivel base, tengo 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 los genéricos de Java no permitirán:
public class Left<L> extends Either<L,?>
No he usado mucho los Enums de Java, pero si bien parecen ser el próximo mejor candidato, no tengo esperanzas.
En este punto, creo que esto solo podría ser posible mediante los valores de Object
conversión de tipo, que espero evitar por completo, a menos que haya una forma de hacerlo una vez, de forma segura, y poder usar eso para todos los tipos de suma.
Cree una clase abstracta con un constructor privado y ancle sus "constructores de datos" (métodos de fábrica estáticos left
y right
) dentro de la clase para que puedan ver el constructor privado pero nada más puede, efectivamente, sellar el tipo.
Use un método abstracto para simular la coincidencia exhaustiva de patrones, anulando adecuadamente en los tipos de concreto devueltos por los métodos estáticos de fábrica. Implemente métodos de conveniencia (como fromLeft
, fromRight
, bimap
, first
, second
) en términos de either
de los 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 intentabas hacer, la class Left<L> extends Either<L,?>
, Considera la firma <A, B> Either<A, B> left(A value)
. El parámetro de tipo B
no aparece en la lista de parámetros. Entonces, dado un valor de algún tipo A
, puede obtener un Either<A, B>
para cualquier tipo B
La herencia se puede usar para emular los tipos de suma (uniones disjuntas), pero hay algunos problemas con los que debe lidiar:
- Debe tener cuidado de evitar que otros agreguen nuevos casos a su tipo. Esto es especialmente importante si desea manejar exhaustivamente cada caso que pueda encontrar. Es posible con una superclase no final y un constructor privado de paquetes.
- La falta de parches de patrón hace que sea bastante difícil consumir un valor de este tipo. Si desea que el compilador compruebe la forma de garantizar que ha manejado exhaustivamente todos los casos, debe implementar una función de coincidencia usted mismo.
- Estás obligado a utilizar uno de los dos estilos de API, ninguno de los cuales es ideal:
- Todos los casos implementan una API común, arrojando errores en la API que no son compatibles. Considera
Optional.get()
. Idealmente, este método solo estaría disponible en un tipo disjunto cuyo valor se sepa que essome
lugar denone
. Pero no hay forma de hacerlo, por lo que es un miembro de instancia de un tipoOptional
general.NoSuchElementException
si lo llama en una opción cuyo "caso" es "ninguno". - Cada caso tiene una API única que le dice exactamente de lo que es capaz, pero que requiere una comprobación de tipo manual y emitir cada vez que desee llamar a uno de estos métodos específicos de subclase.
- Todos los casos implementan una API común, arrojando errores en la API que no son compatibles. Considera
- Cambiar "casos" requiere una nueva asignación de objetos (y agrega presión en el GC si se realiza a menudo).
TL; DR: La programación funcional en Java no es una experiencia agradable.
Una forma estándar de codificación de tipos de suma es la codificación de Boehm-Berarducci (a menudo denominada por el nombre de su primo, codificación Church) que representa un tipo de datos algebraicos 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
lugar de Function<A, R> left, Function<B, R> right
si desea la opción de coincidencia de patrones sin producir un resultado.