programming programacion lenguaje language eta java haskell

java - programacion - haskell jvm



¿Cómo puedo simular "O bien a b" de Haskell en Java? (14)

¿Cómo puedo escribir un método Java seguro que devuelve algo de la clase a o algo de la clase b? Por ejemplo:

public ... either(boolean b) { if (b) { return new Integer(1); } else { return new String("hi"); } }

¿Cuál es la manera más limpia?

(Lo único que se me viene a la mente es usar excepciones, lo que obviamente es malo, ya que está abusando de un mecanismo de manejo de errores para una función de lenguaje general ...

public String either(boolean b) throws IntException { if (b) { return new String("test"); } else { throw new IntException(new Integer(1)); } }

)


Aquí hay una solución de tipo seguro comprobada estáticamente; esto significa que no puede crear errores de tiempo de ejecución. Por favor, lea la oración anterior en la forma en que se entiende. Sí, puedes provocar excepciones de alguna forma u otra ...

Es muy detallado, pero bueno, ¡es Java!

public class Either<A,B> { interface Function<T> { public void apply(T x); } private A left = null; private B right = null; private Either(A a,B b) { left = a; right = b; } public static <A,B> Either<A,B> left(A a) { return new Either<A,B>(a,null); } public static <A,B> Either<A,B> right(B b) { return new Either<A,B>(null,b); } /* Here''s the important part: */ public void fold(Function<A> ifLeft, Function<B> ifRight) { if(right == null) ifLeft.apply(left); else ifRight.apply(right); } public static void main(String[] args) { Either<String,Integer> e1 = Either.left("foo"); e1.fold( new Function<String>() { public void apply(String x) { System.out.println(x); } }, new Function<Integer>() { public void apply(Integer x) { System.out.println("Integer: " + x); } }); } }

Es posible que desee consultar Functional Java y el blog Tony Morris.

Here está el enlace a la implementación de Either en Java funcional. El fold en mi ejemplo se llama allí. Tienen una versión más sofisticada de fold , que es capaz de devolver un valor (que parece apropiado para el estilo de programación funcional).


Basado en la answer de Riccardo, el siguiente fragmento de código funcionó para mí:

public class Either<L, R> { private L left_value; private R right_value; private boolean right; public L getLeft() { if(!right) { return left_value; } else { throw new IllegalArgumentException("Left is not initialized"); } } public R getRight() { if(right) { return right_value; } else { throw new IllegalArgumentException("Right is not initialized"); } } public boolean isRight() { return right; } public Either(L left_v, Void right_v) { this.left_value = left_v; this.right = false; } public Either(Void left_v, R right_v) { this.right_value = right_v; right = true; } }

Uso:

Either<String, Integer> onlyString = new Either<String, Integer>("string", null); Either<String, Integer> onlyInt = new Either<String, Integer>(null, new Integer(1)); if(!onlyString.isRight()) { String s = onlyString.getLeft(); }


Cambia tu diseño para que no necesites esta característica bastante absurda. Cualquier cosa que haría con el valor de retorno requeriría algún tipo de construcción if / else. Sería muy, muy feo.

De una búsqueda rápida en Google, me parece que lo único que comúnmente se usa para el Haskell''s Either es el informe de errores, por lo que parece que las excepciones son para corregir el reemplazo.


Como etiquetaste a Scala, te daré una respuesta de Scala. Solo usa la clase existente de Either . Aquí hay un ejemplo de uso:

def whatIsIt(flag: Boolean): Either[Int,String] = if(flag) Left(123) else Right("hello") //and then later on... val x = whatIsIt(true) x match { case Left(i) => println("It was an int: " + i) case Right(s) => println("It was a string: " + s) }

Esto es completamente seguro para tipos; no tendrá problemas con el borrado ni nada de eso ... Y si simplemente no puede usar Scala, al menos use esto como un ejemplo de cómo puede implementar su propia clase.


De http://blog.tmorris.net/posts/maybe-in-java/ Descubrí que puedes hacer que el constructor de la clase externa sea privado, de modo que solo las clases anidadas puedan crear una subclase. Este truco es tan seguro como el mejor de arriba, pero mucho menos detallado, funciona para cualquier ADT que desee, como la clase de caso de Scala.

public abstract class Either<A, B> { private Either() { } // makes this a safe ADT public abstract boolean isRight(); public final static class Left<L, R> extends Either<L, R> { public final L left_value; public Left(L l) { left_value = l; } public boolean isRight() { return false; } } public final static class Right<L, R> extends Either<L, R> { public final R right_value; public Right(R r) { right_value = r; } public boolean isRight() { return true; } } }

(comenzado desde el código y estilo de la respuesta superior)

Tenga en cuenta que:

  • Las finales en la subclase son opcionales. Sin ellos puedes subtitular Izquierda y Derecha, pero aún no directamente. Por lo tanto, sin la final s Cualquiera tiene ancho limitado pero profundidad ilimitada.

  • Con ADTs como este, no veo ninguna razón para saltar sobre todo el anti- instanceof carro de la banda. Un booleano funciona para Maybe o Either, pero en general instanceof es la mejor y única opción.


Gracias a Derive4J los tipos de datos algebraicos ahora son muy fáciles en Java. Todo lo que tienes que hacer es crear la siguiente clase:

import java.util.function.Function; @Data public abstract class Either<A, B> { Either(){} /** * The catamorphism for either. Folds over this either breaking into left or right. * * @param left The function to call if this is left. * @param right The function to call if this is right. * @return The reduced value. */ public abstract <X> X either(Function<A, X> left, Function<B, X> right); }

Y Derive4J se encargará de crear constructores para los casos de derechos y derechos, así como una sintaxis de coincidencia de patrones alla Haskell, métodos de mapeo para cada lado y más.


Hay una implementación independiente de Either for Java 8 en una pequeña biblioteca, "ambivalence": http://github.com/poetix/ambivalence

Es lo más parecido a la implementación estándar de Scala, por ejemplo, proporciona proyecciones a la izquierda y a la derecha para las operaciones de map y hashMap .

No hay acceso directo a los valores izquierdo o derecho; más bien, join los dos tipos al proporcionar lambdas para mapearlos en un solo tipo de resultado:

Either<String, Integer> either1 = Either.ofLeft("foo"); Either<String, Integer> either2 = Either.ofRight(23); String result1 = either1.join(String::toUpperCase, Object::toString); String result2 = either2.join(String::toUpperCase, Object::toString);

Puedes obtenerlo desde Maven central:

<dependency> <groupId>com.codepoetics</groupId> <artifactId>ambivalence</artifactId> <version>0.2</version> </dependency>


Las sugerencias ya proporcionadas, aunque factibles, no están completas ya que se basan en referencias null y hacen que "O bien" se enmascare como una tupla de valores. Una suma disjunta es obviamente un tipo u otro.

Sugiero echar un vistazo a la implementación de Here ''s Either como un ejemplo.


Lo he implementado de forma similar a Scala de la siguiente manera. Es un poco detallado (es Java, después de todo :)) pero es seguro.

public interface Choice { public enum Type { LEFT, RIGHT } public Type getType(); interface Get<T> { T value(); } } public abstract class Either<A, B> implements Choice { private static class Base<A, B> extends Either<A, B> { @Override public Left leftValue() { throw new UnsupportedOperationException(); } @Override public Right rightValue() { throw new UnsupportedOperationException(); } @Override public Type getType() { throw new UnsupportedOperationException(); } } public abstract Left leftValue(); public abstract Right rightValue(); public static <A, B> Either<A, B> left(A value) { return new Base<A, B>().new Left(value); } public static <A, B> Either<A, B> right(B value) { return new Base<A, B>().new Right(value); } public class Left extends Either<A, B> implements Get<A> { private A value; public Left(A value) { this.value = value; } @Override public Type getType() { return Type.LEFT; } @Override public Left leftValue() { return Left.this; } @Override public Right rightValue() { return null; } @Override public A value() { return value; } } public class Right extends Either<A, B> implements Get<B> { private B value; public Right(B value) { this.value = value; } @Override public Left leftValue() { return null; } @Override public Right rightValue() { return this; } @Override public Type getType() { return Type.RIGHT; } @Override public B value() { return value; } } }

Luego puede pasar Either<A,B> instancias Either<A,B> alrededor de su código. El Type enumeración se usa principalmente en declaraciones de switch .

La creación de Either valores es simple como:

Either<A, B> underTest; A value = new A(); underTest = Either.left(value); assertEquals(Choice.Type.LEFT, underTest.getType()); assertSame(underTest, underTest.leftValue()); assertNull(underTest.rightValue()); assertSame(value, underTest.leftValue().value());

O, en la situación típica donde se usa en lugar de excepciones,

public <Error, Result> Either<Error,Result> doSomething() { // pseudo code if (ok) { Result value = ... return Either.right(value); } else { Error errorMsg = ... return Either.left(errorMsg); } } // somewhere in the code... Either<Err, Res> result = doSomething(); switch(result.getType()) { case Choice.Type.LEFT: // Handle error Err errorValue = result.leftValue().value(); break; case Choice.Type.RIGHT: // Process result Res resultValue = result.rightValue().value(); break; }

Espero eso ayude.


Lo importante es no intentar escribir en un idioma mientras escribes en otro. Por lo general, en Java, quiere poner el comportamiento en el objeto, en lugar de tener un "script" ejecutándose fuera de la encapsulación destruida por los métodos get. No hay contexto para hacer ese tipo de sugerencias aquí.

Una forma segura de lidiar con este pequeño fragmento en particular es escribirlo como una devolución de llamada. Similar a un visitante muy simple.

public interface Either { void string(String value); void integer(int value); } public void either(Either handler, boolean b) throws IntException { if (b) { handler.string("test"); } else { handler.integer(new Integer(1)); } }

Es posible que desee implementar con funciones puras y devolver un valor al contexto de llamada.

public interface Either<R> { R string(String value); R integer(int value); } public <R> R either(Either<? extends R> handler, boolean b) throws IntException { return b ? handler.string("test") : handler.integer(new Integer(1)); }

(Utilice Void (''V'' mayúscula) si desea volver a estar desinteresado en el valor de retorno.)


Lo más cercano que puedo pensar es un contenedor de ambos valores que te permite verificar qué valor se establece y recuperarlo:

class Either<TLeft, TRight> { boolean isLeft; TLeft left; TRight right; Either(boolean isLeft, TLeft left1, TRight right) { isLeft = isLeft; left = left; this.right = right; } public boolean isLeft() { return isLeft; } public TLeft getLeft() { if (isLeft()) { return left; } else { throw new RuntimeException(); } } public TRight getRight() { if (!isLeft()) { return right; } else { throw new RuntimeException(); } } public static <L, R> Either<L, R> newLeft(L left, Class<R> rightType) { return new Either<L, R>(true, left, null); } public static <L, R> Either<L, R> newRight(Class<L> leftType, R right) { return new Either<L, R>(false, null, right); } } class Main { public static void main(String[] args) { Either<String,Integer> foo; foo = getString(); foo = getInteger(); } private static Either<String, Integer> getInteger() { return Either.newRight(String.class, 123); } private static Either<String, Integer> getString() { return Either.newLeft("abc", Integer.class); } }


Mi fórmula general para simular tipos de datos algebraicos es:

  • El tipo es una clase base abstracta, y los constructores son subclases de ese
  • Los datos para cada constructor se definen en cada subclase. (Esto permite que los constructores con diferentes números de datos funcionen correctamente. También elimina la necesidad de mantener invariantes, ya que solo una variable no es nula o algo así).
  • Los constructores de las subclases sirven para construir el valor para cada constructor.
  • Para deconstruirlo, uno usa instanceof para verificar el constructor, y downcast para el tipo apropiado para obtener los datos.

Entonces, para Either ab de los Either ab , sería algo como esto:

abstract class Either<A, B> { } class Left<A, B> extends Either<A, B> { public A left_value; public Left(A a) { left_value = a; } } class Right<A, B> extends Either<A, B> { public B right_value; public Right(B b) { right_value = b; } } // to construct it Either<A, B> foo = new Left<A, B>(some_A_value); Either<A, B> bar = new Right<A, B>(some_B_value); // to deconstruct it if (foo instanceof Left) { Left<A, B> foo_left = (Left<A, B>)foo; // do stuff with foo_left.a } else if (foo instanceof Right) { Right<A, B> foo_right = (Right<A, B>)foo; // do stuff with foo_right.b }


No necesita resolver con la instanceof cheques o campos redundantes. Sorprendentemente, el sistema de tipos de Java proporciona suficientes características para simular los tipos de suma limpiamente.

Fondo

En primer lugar, ¿sabe que cualquier tipo de datos se puede codificar con solo funciones? Se llama codificación de la Iglesia . Por ejemplo, usando la firma Haskell, Either los Either tipos podría definirse de la siguiente manera:

type Either left right = forall output. (left -> output) -> (right -> output) -> output

Puede interpretarlo como "dada una función en el valor izquierdo y una función en el valor correcto, produce el resultado de cualquiera de ellos".

Definición

Ampliando esta idea, en Java podemos definir una interfaz llamada Matcher , que incluye ambas funciones y luego define el tipo de suma en términos de cómo hacer coincidir patrones en él. Aquí está el código completo:

/** * A sum class which is defined by how to pattern-match on it. */ public interface Sum2<case1, case2> { <output> output match(Matcher<case1, case2, output> matcher); /** * A pattern-matcher for 2 cases. */ interface Matcher<case1, case2, output> { output match1(case1 value); output match2(case2 value); } final class Case1<case1, case2> implements Sum2<case1, case2> { public final case1 value; public Case1(case1 value) { this.value = value; } public <output> output match(Matcher<case1, case2, output> matcher) { return matcher.match1(value); } } final class Case2<case1, case2> implements Sum2<case1, case2> { public final case2 value; public Case2(case2 value) { this.value = value; } public <output> output match(Matcher<case1, case2, output> matcher) { return matcher.match2(value); } } }

Uso

Y luego puedes usarlo así:

import junit.framework.TestCase; public class Test extends TestCase { public void testSum2() { assertEquals("Case1(3)", longOrDoubleToString(new Sum2.Case1<>(3L))); assertEquals("Case2(7.1)", longOrDoubleToString(new Sum2.Case2<>(7.1D))); } private String longOrDoubleToString(Sum2<Long, Double> longOrDouble) { return longOrDouble.match(new Sum2.Matcher<Long, Double, String>() { public String match1(Long value) { return "Case1(" + value.toString() + ")"; } public String match2(Double value) { return "Case2(" + value.toString() + ")"; } }); } }

Con este enfoque, incluso puede encontrar un parecido directo con la coincidencia de patrones en idiomas como Haskell y Scala.

Biblioteca

Este código se distribuye como parte de mi biblioteca de tipos compuestos (Sumas y Productos, también conocidos como Uniones y Tuplas) de aries múltiples. Está en GitHub:

https://github.com/nikita-volkov/composites.java


Puede tener una correspondencia cercana con Haskell escribiendo una clase genérica. Either , paramétrico en dos tipos L y R con dos constructores (uno tomando una L y uno tomando una R ) y dos métodos L getLeft() y R getRight() modo que devuelvan el valor pasado al construir o lanzan una excepción.