interfaz - java grafico
¿Por qué no se puede declarar la interfaz Monad en Java? (2)
Antes de comenzar a leer: esta pregunta no se trata de entender las mónadas, sino de identificar las limitaciones del sistema de tipo Java que impide la declaración de una interfaz de Monad
.
En mi esfuerzo por entender las mónadas, leí this respuesta de Eric Lippert en una pregunta que pregunta sobre una explicación simple de las mónadas. Allí, también enumera las operaciones que pueden ejecutarse en una mónada:
- Que hay una manera de tomar un valor de un tipo no amplificado y convertirlo en un valor del tipo amplificado.
- Que hay una manera de transformar las operaciones en el tipo no amplificado en operaciones en el tipo amplificado que obedece las reglas de composición funcional mencionadas anteriormente.
- Que usualmente hay una manera de obtener el tipo no amplificado de vuelta del tipo amplificado. (Este último punto no es estrictamente necesario para una mónada, pero a menudo ocurre que tal operación existe).
Después de leer más sobre las mónadas, identifiqué la primera operación como la función de return
y la segunda operación como la función de bind
. No pude encontrar un nombre de uso común para la tercera operación, por lo que simplemente lo llamaré la función unbox
.
Para comprender mejor las mónadas, seguí adelante e intenté declarar una interfaz de Monad
genérica en Java. Para esto, primero miré las firmas de las tres funciones anteriores. Para la Mónada M
, se ve así:
return :: T1 -> M<T1>
bind :: M<T1> -> (T1 -> M<T2>) -> M<T2>
unbox :: M<T1> -> T1
La función de return
no se ejecuta en una instancia de M
, por lo que no pertenece a la interfaz Monad
. En su lugar, se implementará como un método de construcción o fábrica.
También por ahora, unbox
función unbox
de la declaración de interfaz, ya que no es necesaria. Habrá diferentes implementaciones de esta función para las diferentes implementaciones de la interfaz.
Por lo tanto, la interfaz de Monad
solo contiene la función de bind
.
Intentemos declarar la interfaz:
public interface Monad {
Monad bind();
}
Hay dos defectos:
- La función de
bind
debe devolver la implementación concreta, sin embargo, solo devuelve el tipo de interfaz. Esto es un problema, ya que tenemos las operaciones unbox declaradas en los subtipos concretos. Me referiré a esto como problema 1 . - La función de
bind
debería recuperar una función como parámetro. Nos ocuparemos de esto más adelante.
Utilizando el tipo concreto en la declaración de interfaz.
Esto aborda el problema 1: si mi comprensión de las mónadas es correcta, entonces la función de bind
siempre devuelve una nueva mónada del mismo tipo concreto que la mónada donde se llamó. Entonces, si tengo una implementación de la interfaz de M.bind
llamada M
, entonces M.bind
devolverá otra M
pero no una Monad
. Puedo implementar esto usando genéricos:
public interface Monad<M extends Monad<M>> {
M bind();
}
public class MonadImpl<M extends MonadImpl<M>> implements Monad<M> {
@Override
public M bind() { /* do stuff and return an instance of M */ }
}
Al principio, esto parece funcionar, sin embargo, hay al menos dos defectos con esto:
Esto se descompone tan pronto como una clase de implementación no se proporciona a sí misma sino a otra implementación de la interfaz
Monad
como el parámetro de tipoM
, porque entonces el método debind
devolverá el tipo incorrecto. Por ejemplo elpublic class FaultyMonad<M extends MonadImpl<M>> implements Monad<M> { ... }
devolverá una instancia de
MonadImpl
donde debería devolver una instancia deFaultyMonad
. Sin embargo, podemos especificar esta restricción en la documentación y considerar dicha implementación como un error del programador.El segundo defecto es más difícil de resolver. Lo llamaré problema 2 : cuando intento crear una instancia de la clase
MonadImpl
, necesito proporcionar el tipo deM
Intentemos esto:new MonadImpl<MonadImpl<MonadImpl<MonadImpl<MonadImpl< ... >>>>>()
Para obtener una declaración de tipo válida, esto tiene que continuar infinitamente. Aquí hay otro intento:
public static <M extends MonadImpl<M>> MonadImpl<M> create() { return new MonadImpl<M>(); }
Si bien esto parece funcionar, solo hemos diferido el problema a la llamada. Aquí está el único uso de esa función que funciona para mí:
public void createAndUseMonad() { MonadImpl<?> monad = create(); // use monad }
que esencialmente se reduce a
MonadImpl<?> monad = new MonadImpl<>();
Pero esto claramente no es lo que queremos.
Usando un tipo en su propia declaración con parámetros de tipo desplazado
Ahora, agreguemos el parámetro de función a la función de bind
: Como se describió anteriormente, la firma de la función de bind
ve así: T1 -> M<T2>
. En Java, este es el tipo Function<T1, M<T2>>
. Aquí está el primer intento de declarar la interfaz con el parámetro:
public interface Monad<T1, M extends Monad<?, ?>> {
M bind(Function<T1, M> function);
}
Tenemos que agregar el tipo T1
como parámetro de tipo genérico a la declaración de interfaz, para poder usarlo en la firma de función. El primero ?
es la T1
de la mónada devuelta de tipo M
Para reemplazarlo con T2
, debemos agregar T2
como un parámetro de tipo genérico:
public interface Monad<T1, M extends Monad<T2, ?, ?>,
T2> {
M bind(Function<T1, M> function);
}
Ahora, tenemos otro problema. Agregamos un tercer parámetro de tipo a la interfaz de Monad
, por lo que tuvimos que agregar un nuevo ?
para el uso de la misma. ¿Ignoraremos lo nuevo ?
por ahora investigar el ahora primero ?
. Es la M
de la mónada devuelta de tipo M
Vamos a tratar de eliminar esto ?
cambiando el nombre de M
a M1
e introduciendo otro M2
:
public interface Monad<T1, M1 extends Monad<T2, M2, ?, ?>,
T2, M2 extends Monad< ?, ?, ?, ?>> {
M1 bind(Function<T1, M1> function);
}
La introducción de otros resultados de T3
en:
public interface Monad<T1, M1 extends Monad<T2, M2, T3, ?, ?>,
T2, M2 extends Monad<T3, ?, ?, ?, ?>,
T3> {
M1 bind(Function<T1, M1> function);
}
e introduciendo otros resultados de M3
en:
public interface Monad<T1, M1 extends Monad<T2, M2, T3, M3, ?, ?>,
T2, M2 extends Monad<T3, M3, ?, ?, ?, ?>,
T3, M3 extends Monad< ?, ?, ?, ?, ?, ?>> {
M1 bind(Function<T1, M1> function);
}
¿Vemos que esto continuará para siempre si intentamos resolver todo ?
. Este es el problema 3 .
Resumiendo todo
Identificamos tres problemas:
- Utilizando el tipo concreto en la declaración del tipo abstracto.
- Creación de una instancia de un tipo que se recibe a sí mismo como parámetro de tipo genérico.
- Declarar un tipo que se usa a sí mismo en su declaración con parámetros de tipo desplazado.
La pregunta es: ¿cuál es la característica que falta en el sistema de tipo Java? Como hay idiomas que funcionan con mónadas, estos idiomas tienen que declarar de alguna manera el tipo de Monad
. ¿Cómo estos otros idiomas declaran el tipo Monad
? No pude encontrar información sobre esto. Solo encuentro información sobre la declaración de mónadas concretas, como la mónada Maybe
.
¿Yo me perdí algo? ¿Puedo resolver correctamente uno de estos problemas con el sistema de tipo Java? Si no puedo resolver el problema 2 con el sistema de tipo Java, ¿hay alguna razón por la que Java no me advierte sobre la declaración de tipo no instanciable?
Como ya se dijo, esta pregunta no se trata de entender las mónadas. Si mi comprensión de las mónadas es incorrecta, puede dar una pista al respecto, pero no intente dar una explicación. Si mi comprensión de las mónadas es errónea, los problemas descritos permanecen.
Esta pregunta tampoco es acerca de si es posible declarar la interfaz Monad
en Java. Esta pregunta ya recibió una respuesta de Eric Lippert en su respuesta de SO vinculada arriba: No lo es. Esta pregunta es sobre cuál es exactamente la limitación que me impide hacer esto. Eric Lippert se refiere a esto como tipos superiores, pero no puedo entenderlo.
La mayoría de los lenguajes OOP no tienen un sistema de tipos lo suficientemente rico como para representar directamente el patrón de la mónada; necesita un sistema de tipos que admita tipos que sean tipos superiores a los tipos genéricos. Así que no trataría de hacer eso. Más bien, implementaría tipos genéricos que representan cada mónada e implementaría métodos que representan las tres operaciones que necesita: convertir un valor en un valor amplificado, convertir un valor amplificado en un valor y transformar una función en valores no amplificados en una función Valores amplificados.
¿Cuál es la característica que falta en el sistema de tipo Java? ¿Cómo estos otros idiomas declaran el tipo Monad?
¡Buena pregunta!
Eric Lippert se refiere a esto como tipos superiores, pero no puedo entenderlo.
No estas solo. Pero en realidad no son tan locos como suenan.
Respondamos a las dos preguntas observando cómo Haskell declara el tipo "mónada": verás por qué aparecen las citas en un minuto. Lo he simplificado un poco; El patrón de mónada estándar también tiene un par de otras operaciones en Haskell:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
Chico, parece a la vez increíblemente simple y completamente opaco al mismo tiempo, ¿no es así?
Aquí, déjame simplificar eso un poco más. Haskell le permite declarar su propio operador de infijo para enlace, pero lo llamaremos enlace:
class Monad m where
bind :: m a -> (a -> m b) -> m b
return :: a -> m a
De acuerdo, ahora al menos podemos ver que hay dos operaciones de mónada allí. ¿Qué significa el resto de esto?
Lo primero que debe tener en mente, como nota, es el de los "tipos más amables". (Como Brian señala, simplifiqué un poco esta jerga en mi respuesta original. ¡También es bastante divertido que su pregunta atrajera la atención de Brian!)
En Java, una "clase" es un tipo de "tipo", y una clase puede ser genérica. Así que en Java tenemos int
e IFrob
y List<IBar>
y son todos los tipos.
A partir de este momento, descarte cualquier intuición que tenga acerca de que Giraffe es una clase que es una subclase de Animal, y así sucesivamente; No necesitaremos eso. Piensa en un mundo sin herencia; No volverá a entrar en esta discusión.
¿Qué son las clases en Java? Bueno, la forma más fácil de pensar en una clase es que es un nombre para un conjunto de valores que tienen algo en común , de modo que cualquiera de esos valores puede usarse cuando se requiere una instancia de la clase. Tiene un Point
clase, digamos, y si tiene una variable de tipo Point
, puede asignarle cualquier instancia de Point
. La clase de Point
es, en cierto sentido, solo una forma de describir el conjunto de todas las instancias de Point
. Las clases son una cosa que es más alta que las instancias .
En Haskell también hay tipos genéricos y no genéricos. Una clase en Haskell no es un tipo de tipo. En Java, una clase describe un conjunto de valores ; Cada vez que necesite una instancia de la clase, puede usar un valor de ese tipo. En Haskell una clase describe un conjunto de tipos . Esa es la característica clave que falta en el sistema de tipo Java. En Haskell, una clase es más alta que un tipo, que es más alta que una instancia. Java solo tiene dos niveles de jerarquía; Haskell tiene tres. En Haskell puede expresar la idea "en cualquier momento en que necesite un tipo que tenga ciertas operaciones, puedo usar un miembro de esta clase".
(ASIDE: Quiero señalar aquí que estoy simplificando un poco. Considere en Java, por ejemplo, List<int>
y List<String>
. Estos son dos "tipos", pero Java los considera como una "clase ", así que en cierto sentido, Java también tiene clases que son" más altas "que los tipos. Pero, de nuevo, se podría decir lo mismo en Haskell, que la list x
y la list y
son tipos, y esa list
es una cosa que es más alta que una tipo; es una cosa que puede producir un tipo. Entonces, de hecho, sería más exacto decir que Java tiene tres niveles y Haskell tiene cuatro . El punto sigue siendo: Haskell tiene un concepto para describir las operaciones disponibles en un tipo que es simplemente más potente que Java. Lo veremos con más detalle a continuación.)
Entonces, ¿en qué se diferencia esto de las interfaces? Esto suena a interfaces en Java: necesita un tipo que tenga ciertas operaciones, define una interfaz que describe esas operaciones. Veremos lo que falta en las interfaces de Java.
Ahora podemos empezar a darle sentido a este Haskell:
class Monad m where
Entonces, ¿qué es la Monad
? Es una clase ¿Qué es una clase? Es un conjunto de tipos que tienen algo en común, de manera que cada vez que necesite un tipo que tenga ciertas operaciones, puede usar un tipo de Monad
.
Supongamos que tenemos un tipo que es miembro de esta clase; llamalo m
. ¿Cuáles son las operaciones que deben realizarse en este tipo para que ese tipo sea miembro de la clase Monad
?
bind :: m a -> (a -> m b) -> m b
return :: a -> m a
El nombre de la operación aparece a la izquierda de ::
, y la firma viene a la derecha. Entonces, para ser una Monad
, un tipo m
debe tener dos operaciones: bind
y return
. ¿Cuáles son las firmas de esas operaciones? Veamos primero el return
.
a -> m a
ma
es Haskell para lo que en Java sería M<A>
. Es decir, esto significa que m
es un tipo genérico, a
es un tipo, ma
está parametrizado con a
.
x -> y
en Haskell es la sintaxis de "una función que toma el tipo x
y devuelve el tipo y
". Es Function<X, Y>
.
Juntándolo, y tenemos que return
es una función que toma un argumento de tipo a
y devuelve un valor de tipo ma
. O en java
static <A> M<A> Return(A a);
bind
es un poco más difícil Creo que el OP entiende bien esta firma, pero para los lectores que no están familiarizados con la sintaxis de Haskell, permítanme ampliar esto un poco.
En Haskell, las funciones solo toman un argumento. Si desea una función de dos argumentos, crea una función que toma un argumento y devuelve otra función de un argumento . Así que si tienes
a -> b -> c
Entonces que tienes Una función que toma una a
y devuelve una b -> c
. Supongamos que desea hacer una función que tomó dos números y devolvió su suma. Haría una función que tome el primer número, y devuelva una función que tome un segundo número y lo agregue al primer número.
En java tu dirias
static <A, B, C> Function<B, C> F(A a)
Así que si quisieras una C y tuvieras una A y una B, podrías decir
F(a)(b)
¿Tener sentido?
De acuerdo
bind :: m a -> (a -> m b) -> m b
es efectivamente una función que toma dos cosas: una ma
, y una a -> mb
y devuelve una mb
. O, en Java, es directamente:
static <A, B> Function<Function<A, M<B>>, M<B>> Bind(M<A>)
O, más idiomáticamente en Java:
static <A, B> M<B> Bind(M<A>, Function<A, M<B>>)
Así que ahora ves por qué Java no puede representar el tipo de mónada directamente. No tiene la capacidad de decir "Tengo una clase de tipos que tienen este patrón en común".
Ahora, puedes hacer todos los tipos monádicos que quieras en Java. Lo que no puede hacer es crear una interfaz que represente la idea "este tipo es un tipo de mónada". Lo que necesitas hacer es algo como:
typeinterface Monad<M>
{
static <A> M<A> Return(A a);
static <A, B> M<B> Bind(M<A> m, Function<A, M<B>> f);
}
¿Ves cómo la interfaz de tipo habla del tipo genérico en sí? Un tipo monádico es cualquier tipo M
que sea genérico con un parámetro de tipo y tiene estos dos métodos estáticos . Pero no se puede hacer eso en los sistemas de tipo Java o C #. Bind
supuesto, el Bind
podría ser un método de instancia que toma una M<A>
como this
. Pero no hay manera de hacer que Return
sea nada más que estático. Java no le brinda la posibilidad de (1) parametrizar una interfaz por un tipo genérico no estructurado, y (2) no tiene la capacidad de especificar que los miembros estáticos forman parte del contrato de la interfaz.
Como hay idiomas que funcionan con mónadas, estos idiomas tienen que declarar de alguna manera el tipo de Mónada.
Bueno, lo pensarías pero en realidad no. En primer lugar, por supuesto, cualquier idioma con un sistema de tipos suficiente puede definir tipos monádicos; puede definir todos los tipos monádicos que desee en C # o Java, simplemente no puede decir lo que todos tienen en común en el sistema de tipos. No se puede crear una clase genérica que solo pueda parametrizarse con tipos monádicos, por ejemplo.
Segundo, puedes incrustar el patrón de mónada en el lenguaje de otras maneras. C # no tiene manera de decir "este tipo coincide con el patrón de mónada", pero C # tiene integraciones de consulta (LINQ) integradas en el lenguaje. ¡Las consultas de comprensión funcionan en cualquier tipo monádico! Es solo que la operación de enlace debe llamarse SelectMany
, lo cual es un poco extraño. Pero si miras la firma de SelectMany
, verás que solo es bind
:
static IEnumerable<R> SelectMany<S, R>(
IEnumerable<S> source,
Func<S, IEnumerable<R>> selector)
Esa es la implementación de SelectMany
para la mónada de secuencia, IEnumerable<T>
, pero en C # si escribe
from x in a from y in b select z
entonces el tipo de a
''s puede ser de cualquier tipo monádico, no solo IEnumerable<T>
. Lo que se requiere es que a
sea M<A>
, que b
sea M<B>
, y que haya un SelectMany
adecuado que siga el patrón de la mónada. Así que esa es otra forma de integrar un "reconocedor de mónada" en el lenguaje, sin representarlo directamente en el sistema de tipos.
(El párrafo anterior es en realidad una mentira de la simplificación excesiva; el patrón de enlace utilizado por esta consulta es ligeramente diferente del enlace monádico estándar por razones de rendimiento. Conceptualmente, esto reconoce el patrón de la mónada; en realidad, los detalles difieren ligeramente. Lea acerca de ellos aquí http://ericlippert.com/2013/04/02/monads-part-twelve/ si está interesado.)
Algunos puntos más pequeños:
No pude encontrar un nombre de uso común para la tercera operación, por lo que simplemente lo llamaré la función unbox.
Buena elección; Por lo general, se llama la operación de "extracto". Una mónada no necesita tener expuesta una operación de extracción, pero, por supuesto, de algún modo el bind
debe poder sacar la A
de la M<A>
para llamar a la Function<A, M<B>>
, por lo que lógicamente algunos tipo de operación de extracción por lo general existe.
Una comuna (una mónada hacia atrás, en cierto sentido) requiere que se exponga una operación de extract
; extract
es esencialmente return
hacia atrás. Una comuna también requiere una operación de extend
que es una especie de bind
girado hacia atrás. Tiene la firma static M<B> Extend(M<A> m, Func<M<A>, B> f)
Si observa lo que está haciendo el proyecto AspectJ , es similar a la aplicación de mónadas a Java. La forma en que lo hacen es postprocesar el código de bytes de las clases para agregar la funcionalidad adicional, y la razón por la que tienen que hacerlo es porque no hay forma dentro del lenguaje sin las extensiones de AspectJ para hacer lo que necesitan. hacer; El lenguaje no es lo suficientemente expresivo.
Un ejemplo concreto: digamos que comienza con la clase A. Tiene una mónada M, por lo que M (A) es una clase que funciona igual que A, pero todas las entradas y salidas de los métodos se remontan a log4j. AspectJ puede hacer esto, pero no hay ninguna facilidad dentro del lenguaje Java que lo permita.
En particular, no hay ninguna manera dentro del lenguaje Java de especificar un tipo mediante programación (excepto la manipulación de códigos de byte a la AspectJ). Todos los tipos están predefinidos cuando se inicia el programa.