Programación funcional en Java puro: ejemplos de Functor y Monad

Functional Programming Pure Java

La gran mayoría de los programadores, especialmente aquellos sin experiencia en programación funcional, tienden a pensar que la mónada es una especie de concepto misterioso de la informática, por lo que, en teoría, no ayuda a su carrera como programador. Esta visión negativa se puede atribuir a docenas de artículos o publicaciones de blogs que son demasiado abstractos o demasiado estrechos. Pero resulta que incluso en la biblioteca estándar de Java, las mónadas están en todas partes, especialmente desde el Java Development Kit (JDK) 8 (más sobre esto más adelante). Es absolutamente asombroso que una vez que aprendes sobre las mónadas por primera vez, de repente habrá algunas clases y abstracciones completamente diferentes que no están relacionadas en absoluto para diferentes propósitos.
Monad resume varios conceptos aparentemente independientes, por lo que se necesita muy poco tiempo para aprender otra encarnación de Monad. Por ejemplo, no tiene que aprender cómo funciona CompletableFuture en Java 8; una vez que se da cuenta de que es una mónada, puede saber exactamente cómo funciona y qué puede esperar de su semántica. Entonces escucharás que RxJava suena muy diferente, pero como Observable es una mónada, no hay mucho que agregar. Sin saberlo, ha encontrado muchos otros ejemplos de Mónadas. Por lo tanto, incluso si no está utilizando RxJava, esta sección será una revisión útil.
Functors
Antes de explicar qué es una mónada, estudiemos una estructura simple llamada funtor. Los funciones son estructuras de datos tipificadas que encapsulan ciertos valores. Desde un punto de vista gramatical, los Functors son contenedores con las siguientes API:
import java.util.function.Function
Functor de interfaz {
Mapa de funciones (función f)
}
Pero la sintaxis por sí sola no es suficiente para entender qué son los Functors. La única operación proporcionada por el functor es map () con la función f. Esta función recibe cualquier cosa en el cuadro, lo convierte y envuelve el resultado en otros Functors como está. Por favor lea cuidadosamente. Functor es siempre un contenedor inmutable, por lo que map no muta el objeto original que realiza la operación. En su lugar, devolverá el resultado (o el resultado, tenga paciencia) envuelto en nuevos Functors, los Functors pueden ser de tipo R. Además, los Functors no deben realizar ninguna operación al aplicar la función de identificación (es decir, map (x-> x )). Este modo siempre debe devolver los mismos Functors o instancias iguales.
El functor generalmente se compara con la instancia que contiene T, y la única forma de interactuar con este valor es convertirlo. Sin embargo, no existe una forma idiomática de desatarse o escapar de los Functors. El valor siempre está en el contexto de Functors. ¿Por qué son útiles los Functors? Usan una API unificada que es aplicable a todas las colecciones para resumir varios modismos comunes, como colecciones, promesas y opcionales. Permítanme presentarles algunos Functors para que utilicen esta API de manera más fluida:
Functor de interfaz{
Mapa F (Función f)
}
class Identity implementa Functor{
valor T final privado
Identidad (valor T) {this.value = value}
mapa de identidad pública (función f) {
resultado final R = f. aplicar (valor)
devolver nueva identidad (resultado)
}
}
requiere parámetros de tipo F adicionales para la compilación de identidades. En el ejemplo anterior, vio los Functores más simples, que contienen solo un valor. Solo puede convertirlo dentro del método del mapa, pero no puede extraerlo. Esto se considera que está más allá del alcance de los Functors puros. La única forma de interactuar con Functors es aplicar secuencias de conversión de tipo seguro:
Identidad idString = nueva identidad ('abc')
Identidad idInt = idString.map (Cadena :: longitud)
o con fluidez, como si estuvieras escribiendo una función:
Identidad idBytes = nueva identidad (cliente)
.map (Cliente :: getAddress)
.map (Dirección :: calle)
.map ((String s) -> s.substring (0, 3))
.map (Cadena :: toLowerCase)
.map (String :: getBytes)
Desde esta perspectiva, el mapeo en Functors no es muy diferente de llamar a funciones encadenadas:
byte [] bytes = cliente
.getAddress ()
.calle()
.substring (0, 3)
.toLowerCase ()
.getBytes ()
¿Por qué se molestaría con un paquete tan extenso, que no solo no proporciona ningún valor agregado, sino que tampoco puede extraer el contenido? Bueno, resulta que puedes usar esta abstracción de Functors original para modelar varios otros conceptos. Por ejemplo, a partir de Java 8, los Functors con el método map () son opcionales. Implementémoslo desde cero:
clase F Implementos opcionales Functor{
valor T final privado OrNull
private FOptional (T valueOrNull) {
this.valueOrNull = valueOrNull
}
mapa público FOptional (Función f) {
si (valueOrNull == null)
volver vacío ()
demás
retorno de (f.apply (valueOrNull))
}
Public static FOptional of (T a) {
devolver nuevo FOptional (a)
}
public static FOptional empty () {
devolver nuevo FOptional (nulo)
}
}
Ahora es divertido. Un funtor FOptional puede contener valor, pero también puede estar vacío. Esta es una codificación nula de tipo seguro. Hay dos métodos de construcción P: proporcionando valores o creando instancias vacías (). En ambos casos, al igual que con Identity, FOptional es inmutable y solo podemos interactuar con valores internos. La diferencia FOptional es que si la función de conversión f está vacía, no se puede aplicar a ningún valor. Esto significa que los Functors no necesariamente tienen que encapsular completamente un valor T de tipo. También puede envolver cualquier número de valores, al igual que List ... functor:
import com.google.common.collect.ImmutableList
clase FList implementa Functor{
lista ImmutableList final privada
FList (valor iterable) {
this.list = ImmutableList.copyOf (valor)
}
@Anular
mapa FList público (Función f) {
Resultado de ArrayList = new ArrayList (list.size ())
para (T t: lista) {
resultado.add (p. ej., aplicar (t))
}
devolver nueva FList (resultado)
}
}
La API sigue siendo la misma: puede usar Functors en la transición, pero el comportamiento es muy diferente. Ahora, transformamos cada elemento en FList y transformamos la lista completa de manera declarativa. Entonces, si tienes una lista de clientes y quieres una lista de sus calles, es muy simple:
importar estático java.util.Arrays.asList
Clientes de FList = nuevo FList (asList (cust1, cust2))
FList calles = clientes
.map (Cliente :: getAddress)
.map (Dirección :: calle)
Esto ya no es tan simple como decir customers.getAddress (). street (), no puede getAddress () en una colección de clientes, debe getAddress () en cada cliente individual en la llamada y luego volver a ponerlo en una colección. Por cierto, Groovy descubrió que este patrón es tan común que en realidad tiene un azúcar sintáctico: cliente * .getAddress () *. Calle (). Este operador se llama dispersión, que en realidad es un disfraz de mapa. Tal vez quiera saber por qué tengo que iterar el mapa manualmente dentro de la lista en lugar de usar s list.stream (). Map (f) .collect (toList ()) en StreamJava 8? ¿Suena este? ¿Qué pasa si mi java.util.stream.Stream te dice que también eres Functor en Java? Por cierto, ¿una Mónada?

Ahora, debería ver el primer beneficio de los Functors: abstraen la representación interna y proporcionan una API consistente y fácil de usar para varias estructuras de datos. Como ejemplo final, permítanme presentarles una función de promesa similar Future. La promesa de 'compromiso' algún día proporcionará un valor. Aún no ha aparecido, puede ser porque se han producido algunos cálculos de fondo o estamos esperando eventos externos. Pero aparecerá en el futuro. El mecanismo para completar una Promesa no es interesante, pero la naturaleza de los Functors es:
Promesa al cliente = //…
Bytes de promesa = cliente
.map (Cliente :: getAddress)
.map (Dirección :: calle)
.map ((String s) -> s.substring (0, 3))
.map (Cadena :: toLowerCase)
.map (String :: getBytes)
¿Luce familiar? ¡Esto es lo que quiero decir! La realización de Functors está más allá del alcance de este artículo y ni siquiera es importante. No hace falta decir que estamos muy cerca de implementar CompletableFuture desde Java 8, y casi encontramos Observable desde RxJava. Pero volvamos a Functors. Promise aún no ha tenido el valor del cliente. Se espera que tenga este valor en el futuro. Sin embargo, todavía podemos mapear dichos Functors con la misma sintaxis y semántica que usando FOptional y FList. El comportamiento sigue lo que dijo Functors. Llamar a customer.map (Customer :: getAddress) producirá una Promise



, Lo que significa que el mapa no está bloqueado. customer.map () completará el compromiso del cliente. En cambio, devolverá otra promesa de un tipo diferente. Cuando se completa el compromiso ascendente, el compromiso descendente aplica la función pasada a map () y pasa el resultado al flujo descendente. De repente, nuestros Functors nos permiten canalizar cálculos asincrónicos sin bloqueos. Pero no es necesario que comprenda o aprenda, ya que las promesas son funciones, debe seguir la gramática y las reglas.
Los functors tienen muchos otros buenos ejemplos, como representar valores o errores en combinación. Pero ahora es el momento de mirar a las mónadas.
De los functores a las mónadas

Supongo que comprende cómo funcionan los Functors y por qué son abstracciones útiles. Pero los Functors no están tan extendidos como cabría esperar. ¿Qué sucede si su función de conversión (pasada como parámetro de map ()) devuelve una instancia de Functors en lugar de un valor simple? Bueno, Functors también es un valor, por lo que no suceden cosas malas. Vuelva a poner todo en Functors para que todos los comportamientos sean consistentes. Sin embargo, suponga que tiene los siguientes métodos convenientes para analizar cadenas:
TryParse opcional (String s) {
tratar {
final int i = Integer.parseInt (s)
volver FOpcional. de (i)
} catch (NumberFormatException e) {
return FOptional.empty ()
}
}
La excepción son los efectos secundarios que afectan el sistema de tipos y la pureza funcional. En lenguajes puramente funcionales, no hay excepciones. Después de todo, nunca hemos oído hablar de lanzar una excepción en una clase de matemáticas, ¿verdad? Los errores y las condiciones ilegales utilizan valores y envoltorios para expresarse con claridad. Por ejemplo, tryParse () acepta un String en lugar de simplemente devolver un int o generar silenciosamente una excepción en tiempo de ejecución. A través del sistema de tipos, le decimos explícitamente a tryParse () que puede fallar y que no hay ninguna excepción o error en el error de formato de cadena. Esta semifalla se indica mediante el resultado opcional. Curiosamente, Java ha verificado las excepciones que deben declararse y manejarse, por lo que, en cierto sentido, Java es más puro en este sentido, no tiene efectos secundarios ocultos. Pero para las excepciones que generalmente no se recomiendan para la inspección en Java, volvamos a tryParse (). Parece útil componer tryParse con String ya envuelto en FOptional:
FOptional str = FOptional.of ('42')
Número opcional = str.map (esto :: tryParse)
Esto no es de extrañar. Si tryParse () devuelve a, int obtendrá FOptional num, pero dado que la función de map () FOptional regresa, envuélvala dos veces en un FOptional incómodo. Verifique el tipo con cuidado, debe comprender por qué obtenemos este paquete doble aquí. Además de parecer aterrador, poner un Functor en Functors destruirá la composición y suavizará los enlaces:
Num1 opcional = //…
Num2 opcional = //…
F fecha opcional1 = num1.map (t -> nueva fecha (t))
// ¡no se compila!
F fecha opcional2 = num2.map (t -> nueva fecha (t))
Aquí, probamos FOptional para mapear contenido convirtiendo int a + Date +. Con int-> Date podemos convertir fácilmente Functor a Functor, sabemos cómo funciona. Pero cuando la situación num2 se vuelve complicada. Lo que num2.map () recibe entrada ya no es un int, pero una FOoption obviamente java.util.Date no tiene tal estructura. Rompimos Functors con doble envoltura. Sin embargo, es muy común tener funciones que devuelvan Functores en lugar de valores simples (como tryParse ()), y no podemos simplemente ignorar este requisito. Un método es introducir un método join () especial sin parámetros para 'aplanar' los Functores anidados:
Num3 opcional = num2.join ()
Funciona, pero debido a que este patrón es tan común, flatMap () introduce un método especial llamado. flatMap () es muy similar al siguiente, map pero espera que la función recibida como parámetro devuelva Functors- o mónadas para ser precisos:
interfaz Monadextiende Functor {
M flatMap (Función f)
}
Simplemente concluimos que este flatMap es solo un azúcar sintáctico que puede mejorar los ingredientes. Pero el método flatMap (a menudo llamado Haskell bind o >> = llamado desde Haskell) es completamente diferente porque permite que se construyan transformaciones complejas con estilos funcionales puros. Si FOptional es una instancia de mónada, la resolución puede proceder repentinamente como se esperaba:
FOpcional num = FOptional.of ('42')
Respuesta opcional = num.flatMap (this :: tryParse)
Las mónadas no necesitan implementar map, se puede implementar fácilmente con flatMap (). De hecho flatMap, el operador esencial puede lograr un campo de conversión completamente nuevo. Obviamente, al igual que los Functors, el cumplimiento sintáctico no es suficiente para llamar a una determinada clase Monads. Los operadores FlatMap () deben obedecer las reglas de Monads, pero son muy intuitivos, al igual que la combinación de flatMap () e identidad. Este último requiere que m (x) .flatMap (f) yf (x) contengan el valor x y cualquier mónada de la función f. No profundizaremos en la teoría de las mónadas, pero centrémonos en el significado real. Por ejemplo, cuando la estructura interna del mono no es importante, iluminarán la mónada que la Promesa tendrá valor en el futuro. ¿Puede adivinar por el sistema de tipos cómo se ejecutará Promise en el siguiente programa? Primero, todos los métodos que pueden tomar algún tiempo para completar devuelven una Promesa:
importar java.time.DayOfWeek
Promise loadCustomer (int id) {
//…
}
Promise readBasket (Cliente cliente) {
//…
}
Promise CalculateDiscount (Cesta de la cesta, DayOfWeek dow) {
//…
}
Ahora, podemos escribir estas funciones de una manera que evite todas estas funciones al igual que usar el operador monádico:
Promesa de descuento =
loadCustomer (42)
.flatMap (este :: readBasket)
.flatMap (b -> calculateDiscount (b, DayOfWeek.FRIDAY))
Esto se vuelve muy interesante. flatMap () debe conservar el tipo Monads, por lo que todos los objetos intermedios son Promises. No se trata solo de mantener los tipos en orden, ¡el programa anterior de repente se volvió completamente asincrónico! loadCustomer () devuelve una Promesa para que no se bloquee. readBasket () acepta cualquier cosa que tenga (tendrá) una Promise y aplica una función Promise que devuelve otra función, y así sucesivamente. Básicamente, hemos establecido una canalización de computación asincrónica, donde completar un paso en segundo plano activará automáticamente el siguiente paso.
Explorar flatMap ()
Es común tener dos Mónadas y combinar los valores que contienen. Sin embargo, ni los Functors ni las mónadas permiten el acceso directo a su interior, lo cual es impuro. Por el contrario, debemos aplicar la conversión con cuidado y no podemos escapar de la mónada. Suponga que tiene dos Mónadas y desea fusionarlas:
importar java.time.LocalDate
importar java.time.Month
Mes de la mónada = //…
Monad dayOfMonth = //…
Fecha de la mónada = month.flatMap ((Mes m) ->
dia del mes
.map ((int d) -> LocalDate. de (2016, m, d)))
Tómese un momento para estudiar el pseudocódigo anterior. No utilizo ninguna implementación de mónada real, Promise y List enfatizan los conceptos centrales. Tenemos dos mónadas independientes, una es de tipo Mes y la otra es de tipo Integer. Para construir LocalDate, debemos construir una transformación anidada que pueda acceder al interior de las dos mónadas. Estudie estos tipos detenidamente, especialmente para asegurarse de que comprende por qué nuestro flatMap usa map () en un lugar y en otro. Piense en cómo construiría este código Monad si también tuviera un tercero. Es muy común aplicar funciones de dos parámetros (de este modo myd en nuestro ejemplo). En Haskell hay una función auxiliar especial llamada liftM2 que implementa la conversión entre map y flatMap. En la pseudogramática de Java, se ve así:
Monad liftM2 (Monad t1, Monad t2, BiFunction fun) {
devolver t1.flatMap ((T1 tv1) ->
t2.map ((T2 tv2) -> fun.apply (tv1, tv2))
)
}
No tiene que implementar este método para cada mónada, este flatMap () es suficiente y funciona de manera consistente para todas las mónadas. liftM2 es muy útil cuando consideras cómo usarlo con varias mónadas. Por ejemplo, listM2 (list1, list2, function) se aplicará a todos los pares de elementos posibles en la función (producto cartesiano). Por otro lado, para las opciones opcionales, solo cuando ambas opciones opcionales no estén vacías, aplicará la función. Aún mejor, para las mónadas, cuando ambas se completan, la función se ejecutará de forma asincrónica. Esto significa que acabamos de inventar un mecanismo de sincronización simple (en el algoritmo de unión de bifurcación), que contiene dos pasos asincrónicos. list1list2Promise Promisejoin ()
Otro operador útil que podemos construir fácilmente, flatMap (), es filter (Predicate), que acepta todo en la mónada y lo descarta por completo si no se cumplen ciertos predicados. En cierto modo, es similar al mapeo map1 a 1, en lugar del mapeo 1 a 1. El mismo filtro (), cada mónada tiene la misma semántica, pero dependiendo de la mónada que usemos, su función es muy buena. Evidentemente, permite filtrar ciertos elementos de la lista:
FList vips =
clientes.filtro (c -> c.totalOrders> 1_000)
Pero también funciona bien, por ejemplo, para elementos opcionales. En este caso, si el contenido opcional no cumple determinadas condiciones, podemos convertir opcional no nulo en vacío. Las piezas opcionales vacías permanecen sin cambios.
De la lista de Mónadas a la lista de Mónadas
Otro operador útil derivado de flatMap () es sequence (). Puede adivinar fácilmente lo que hace mirando la firma de tipo:
Secuencia de mónadas (mónadas iterables)
Normalmente, tenemos un montón de mónadas del mismo tipo y queremos una mónada con una lista de ese tipo. Esto puede sonarle abstracto, pero es muy útil. Imagine que desea cargar algunos clientes de la base de datos al mismo tiempo por ID, por lo que cargaCustomer (id) varias veces para usar diferentes métodos de ID, y cada llamada devuelve una Promise. Ahora, tiene una lista, Promesa, pero lo que realmente desea es una lista de clientes, como la lista de clientes que se mostrará en un navegador web. El operador sequence () (en RxJava sequence () se llama concat () o merge () dependiendo del uso) se acaba de construir como:
FList custPromises = FList
. de (1, 2, 3)
.map (base de datos :: loadCustomer)
Clientes de promesa = custPromises.sequence ()
clientes.map ((FList c) ->…)
Al llamar a cada ID, FList, tenemos un mapa de ID de cliente representativo (¿Sabes cómo ayuda al functor de FList?) Database.loadCustomer (id). Esto hace que la lista de promesas sea muy incómoda. sequence () ahorra un día, pero esto no es solo azúcar sintáctico nuevamente. El código anterior es completamente no bloqueante. Para diferentes tipos de Monadssequence () sigue siendo significativo, pero en diferentes entornos informáticos. Por ejemplo, puede cambiar FList a FOptional. Por cierto, puede implementar sequence () (como map ()) en flatMap ().
flatMap () En términos generales, esto es solo la punta del iceberg. Aunque derivan de la teoría de categorías oscuras, incluso en lenguajes de programación orientados a objetos como Java, las mónadas han demostrado ser abstracciones extremadamente útiles. Las funciones que se pueden componer para devolver funciones de Monads son tan útiles que docenas de clases no relacionadas siguen el comportamiento de Monads.
Además, una vez que los datos se encapsulan en una mónada, suele ser difícil sacarlos explícitamente. Esta operación no es parte del comportamiento de la mónada y a menudo da como resultado un código no idiomático. Por ejemplo, Promise.get () en Promise puede devolver T técnicamente, pero solo a través del bloqueo, y todos los operadores basados ​​en flatMap () no son bloqueantes. Otro ejemplo es FOptional.get (), pero puede fallar porque FOptional puede estar vacío. Incluso FList.get (idx) se asoma a elementos específicos de la lista suena extraño, porque a menudo puede reemplazar el mapa de bucle for ().
Espero que entiendas por qué estas Mónadas son tan populares ahora. Incluso en lenguajes orientados a objetos como Java, son abstracciones muy útiles.
Finalmente, después de tantos años de desarrollo, también he resumido un conjunto de materiales y preguntas de entrevistas para aprender Java. Si quieres mejorarte en tecnología, puedes seguirme, enviar un mensaje privado para recibir información o dejar tu información de contacto en el área de comentarios Recuerda ayudarme a ordenar el repost y dejar que más gente lo vea. imagen