Introducci贸n a la programaci贸n Funcional

1

驴Qu茅 es la Programaci贸n Funcional?

Entendiendo las partes de la programaci贸n funcional

2

驴Qu茅 es una funci贸n en Java?

3

Funciones como ciudadanos de primera clase

4

Funciones puras

5

Entendiendo los efectos secundarios

6

Funciones de orden mayor

7

Funciones lambda

8

Inmutabilidad

Functional Programming en Java

9

Repositorio del curso

10

Configuraci贸n del entorno de trabajo

11

Revisando el paquete java.util.function: Function

12

Revisando el paquete java.util.function: Predicate

13

Revisando el paquete java.util.function: Consumer y Supplier

14

Revisando el paquete java.util.function: Operators y BiFunction

15

Entendiendo dos jugadores clave: SAM y FunctionalInterface

16

Operador de Referencia

17

Analizando la inferencia de tipos

18

Comprendiendo la sintaxis de las funciones lambda

19

Usando metodos default en nuestras interfaces

20

D谩ndole nombre a un viejo amigo: Chaining

21

Entendiendo la composici贸n de funciones

Optional y Streams: Datos mas interesantes

22

La clase Optional

23

Entendiendo los Streams

24

驴Qu茅 son los Stream listeners?

25

Operaciones y Collectors

26

Streams de tipo espec铆fico y Paralelismo

27

Operaciones Terminales

28

Operaciones Intermedias

29

Collectors

Todo junto: Proyecto Job-search

30

job-search: Un proyecto para encontrar trabajo

31

Vista r谩pida a un proyecto de Gradle

32

Revisando las opciones para nuestro CLI

33

Librer铆as adicionales para nuestro proyecto

34

Entendiendo la API de jobs

35

Dise帽ando las Funciones Constructoras de nuestro Proyecto

36

Agregando validaciones de datos

37

Dise帽ando las funciones de transformacion de datos

38

Creando flujos extras de transformaci贸n de Datos

Conclusiones

39

Un repaso a lo aprendido

Operaciones Intermedias

28/39

Lectura

Operaciones intermedias

En clases anteriores hablamos de dos tipos de operaciones: intermedias y finales. No se explicaron a profundidad, pero en esta lectura iremos m谩s a fondo en las operaciones intermedias y trataremos de entender qu茅 sucede por dentro de cada una.

驴Qu茅 son?

Se le dice operaci贸n intermedia a toda operaci贸n dentro de un Stream que como resultado devuelva un nuevo Stream. Es decir, tras invocar una operaci贸n intermedia con un cierto tipo de dato, obtendremos como resultado un nuevo Stream conteniendo los datos ya modificados.

El Stream que recibe la operaci贸n intermedia pasa a ser 鈥consumido鈥 posterior a la invocaci贸n de la operaci贸n, quedando inutilizable para posteriores operaciones. Si decidimos usar el Stream para alg煤n otro tipo de operaciones tendremos un IllegalStateException.

Vi茅ndolo en c贸digo con un ejemplo debe ser mas claro:

Stream initialCourses = Stream.of("Java", "Spring", "Node.js");

Stream lettersOnCourses = initialCourses.map(course -> course.length());
//De este punto en adelante, initialCourses ya no puede agregar mas operaciones.

Stream evenLengthCourses = lettersOnCourses.filter(courseLength -> courseLength % 2 == 0);
//lettersOnCourses se consume en este punto y ya no puede agregar mas operaciones. No es posible usar el Stream mas que como referencia.

El ejemplo anterior puede ser reescrito usando chaining. Sin embargo, la utilidad de este ejemplo es demostrar que las operaciones intermedias generan un nuevo Stream.

Operaciones disponibles

La interfaz Stream cuenta con un grupo de operaciones intermedias. A lo largo de esta lectura explicaremos cada una de ellas y trataremos de aproximar su funcionalidad. Cada operaci贸n tiene implementaciones distintas seg煤n la implementaci贸n de Stream, en nuestro caso, haremos solo aproximaciones de la l贸gica que sigue la operaci贸n.

Las operaciones que ya est谩n definidas son:

  • filter(鈥)
  • map(鈥)
  • flatMap(鈥)
  • distinct(鈥)
  • limit(鈥)
  • peek(鈥)
  • skip(鈥)
  • sorted(鈥)

Analicemos qu茅 hace cada una de ellas y hagamos c贸digo que se aproxime a lo que hacen internamente.

filter

La operaci贸n de filtrado de Stream tiene la siguiente forma:

Stream filter(Predicatesuper T> predicate)

Algunas cosas que podemos deducir 煤nicamente viendo los elementos de la operaci贸n son:

  • La operaci贸n trabaja sobre un Stream y nos devuelve un nuevo Stream del mismo tipo (T)
  • Sin embargo, el Predicate que recibe como par谩metro trabaja con elementos de tipo T y cualquier elemento siempre que sea un subtipo de T. Esto quiere decir que si tenemos la clase PlatziStudent extends Student y tenemos un Stream donde tambi茅n tenemos elementos de tipo PlatziStudent, podemos filtrarlos sin tener que revisar o aclarar el tipo
  • Predicate es una @FunctionalInterface (como lo viste en la clase 11), lo cual nos permite pasar como par谩metro objetos que implementen esta interfaz o lambdas

El uso de esta operaci贸n es sencillo:

public Stream getJavaCourses(List courses){
    return courses.stream()
        .filter(course -> course.contains("Java"));
}

Lo interesante radica en la condici贸n que usamos en nuestra lambda, con ella determinamos si un elemento debe permanecer o no en el Stream resultante. En la lectura anterior hicimos una aproximaci贸n de la operaci贸n filter:

public Stream filter(Predicate predicate) {
    List filteredData = new LinkedList<>();
    for(T t : this.data){
        if(predicate.test(t)){
            filteredData.add(t);
        }
    }

    return filteredData.stream();
}

filter se encarga de iterar cada elemento del Stream y evaluar con el Predicate si el elemento debe estar o no en el Stream resultante. Si nuestro Predicate es sencillo y no incluye ning煤n ciclo o llamadas a otras funciones que puedan tener ciclos, la complejidad del tiempo es de O(n), lo cual hace que el filtrado sea bastante r谩pido.

Usos comunes de filter es limpiar un Stream de datos que no cumplan un cierto criterio. Como ejemplo podr铆as pensar en un Stream de transacciones bancarias, mantener el Stream solo aquellas que superen un cierto monto para mandarlas a auditoria, de un grupo de calificaciones de alumnos filtrar 煤nicamente por aquellos que aprobaron con una calificaci贸n superior a 6, de un grupo de objetos JSON conservar aquellos que tengan una propiedad en especifico, etc.

Entre mas sencilla sea la condici贸n de filtrado, m谩s legible sera el c贸digo. Te recomiendo que, si tienes m谩s de una condici贸n de filtrado, no le temas a usar varias veces filter:

courses.filter(course -> course.getName().contains("Java"))
    .filter(course -> course.getDuration() > 2.5)
    .filter(course -> course.getInstructor().getName() == Instructors.SINUHE_JAIME)

Tu c贸digo sera m谩s legible y las razones de por qu茅 est谩s aplicando cada filtro tendr谩n m谩s sentido. Como algo adicional podr铆as mover esta l贸gica a funciones individuales en caso de que quieras hacer m谩s legible el c贸digo, tener m谩s facilidad de escribir pruebas o utilices en m谩s de un lugar la misma l贸gica para algunas lambdas:

courses.filter(Predicates::isAJavaCourse)
    .filter(Predicates::hasEnoughDuration)
    .filter(Predicates::hasSinuheAsInstructor);

// La l贸gica es la misma:
public final class Predicates {
    public static final boolean isAJavaCourse(Course course){
        return course.getName().contains("Java");
    }
}

map

La operaci贸n map puede parecer complicada en un principio e incluso confusa si estas acostumbrado a usar Map, pero cabe resaltar que no hay relaci贸n entre la estructura y la operaci贸n. La operaci贸n es meramente una transformaci贸n de un tipo a otro.

Stream map(Functionsuper T, ? extends R> mapper)

Los detalles a resaltar son muy similares a los de filter, pero la diferencia clave est谩 en T y R. Estos generics nos dicen que map va a tomar un tipo de dato T, cualquiera que sea, le aplicara la funci贸n mapper y generara un R.

Es algo similar a lo que hac铆as en la secundaria al convertir en una tabla datos, para cada x aplicabas una operaci贸n y obten铆as una y (algunos llaman a esto tabular). map operar谩 sobre cada elemento en el Stream inicial aplicando la Function que le pases como lambda para generar un nuevo elemento y hacerlo parte del Stream resultante:

Stream ids = DatabaseUtils.getIds().stream();

Stream users = ids.map(id -> db.getUserWithId(id));

O, puesto de otra forma, por cada DatabaseID en el Stream inicial, al aplicar map genera un User:

  • DatabaseID(1234) -> map(鈥) -> User(Sinuhe Jaime, @Sierisimo)
  • DatabaseID(4321) -> map(鈥) -> User(Diego de Granda, @degranda10)
  • DatabaseID(007) -> map(鈥) ->User(Oscar Barajas, @gndx)

Esto resulta bastante practico cuando queremos hacer alguna conversi贸n de datos y realmente no nos interesa el dato completo (solo partes de 茅l) o si queremos convertir a un dato complejo partiendo de un dato base.

Si quisi茅ramos replicar qu茅 hace internamente map ser铆a relativamente sencillo:

public  Stream filter(Function mapper) {
    List mappedData = new LinkedList<>();
    for(T t : this.data){
        R r = mapper.apply(t);
        mappedData.add(r);
    }

    return mappedData.stream();
}

La operaci贸n map parece simple ya vista de esta manera. Sin embargo, por dentro de las diferentes implementaciones de Stream hace varias validaciones y optimizaciones para que la operaci贸n pueda ser invocada en paralelo, para prevenir algunos errores de conversi贸n de tipos y hacer que sea mas r谩pida que nuestra versi贸n con un for.

flatMap

En ocasiones no podremos evitar encontrarnos con streams del tipo Stream>, donde tenemos datos con muchos datos鈥

Este tipo de streams es bastante com煤n y puede pasarte por multiples motivos. Se puede tornar dif铆cil operar el Stream inicial si queremos aplicar alguna operaci贸n a cada uno de los elementos en cada una de las listas.

Si mantener la estructura de las listas (o colecciones) no es importante para el procesamiento de los datos que contengan, entonces podemos usar flatMap para simplificar la estructura del Stream, pas谩ndolo de Stream> a Stream.

Visto en un ejemplo m谩s 鈥渧isual鈥:

Stream> coursesLists; // Stream{List["Java", "Java 8 Functional", "Spring"], List["React", "Angular", "Vue.js"], List["Big Data", "Pandas"]}
Stream allCourses; // Stream{ ["Java", "Java 8 Functional", "Spring", "React", "Angular", "Vue.js", "Big Data", "Pandas"]}

flatMap tiene la siguiente forma:

 Stream flatMap(Functionsuper T, ? extends Stream> mapper)

Lo interesante es que el resultado de la funci贸n mapper debe ser un Stream. Stream usar谩 el resultado de mapper para 鈥渁cumular鈥 elementos en un Stream desde otro Stream. Puede sonar confuso, por lo que ejemplificarlo nos ayudar谩 a entenderlo mejor:

//Tenemos esta clase:
public class PlatziStudent {
    private boolean emailSubscribed;
    private List emails;

    public boolean isEmailSubscribed() {
        return emailSubscribed;
    }

    public List getEmails(){
        return new LinkedList<>(emails); //Creamos una copia de la lista para mantener la clase inmutable por seguridad
    }
}

//Primero obtenemos objetos de tipo usuario registrados en Platzi:
Stream platziStudents = getPlatziUsers().stream();

// Despues, queremos enviarle un correo a todos los usuarios pero鈥 solo nos interesa obtener su correo para notificarlos:
Stream allEmailsToNotify = 
                        platziStudents.filter(PlatziStudent::isEmailSubscribed) //Primero evitamos enviar correos a quienes no est茅n subscritos
                                    .flatMap(student -> student.getEmails().stream()); // La lambda genera un nuevo Stream de la lista de emails de cada studiante.

sendMonthlyEmails(allEmailsToNotify);
//El Stream final solo es un Stream de emails, sin mas detalles ni informaci贸n adicional.

flatMap es una manera en la que podemos depurar datos de informaci贸n adicional que no sea necesaria.

distinct

Esta operaci贸n es simple:

Stream distinct()

Lo que hace es comparar cada elemento del Stream contra el resto usando el m茅todo equals. De esta manera, evita que el Stream contenga elementos duplicados. La operaci贸n, al ser intermedia, retorna un nuevo Stream donde los elementos son 煤nicos. Recuerda que para garantizar esto es recomendable que sobrescribas el m茅todo equals en tus clases que representen datos.

limit

La operaci贸n limit recibe un long que determina cu谩ntos elementos del Stream original seran preservados. Si el n煤mero es mayor a la cantidad inicial de elementos en el Stream, b谩sicamente, todos los elementos seguir谩n en el Stream. Un detalle interesante es que algunas implementaciones de Stream pueden estar ordenadas (sorted()) o expl铆citamente no ordenadas (unordered()), lo que puede cambiar dr谩sticamente el resultado de la operaci贸n y el performance.

Stream limit(long maxSize)

La operaci贸n asegura que los elementos en el Stream resultante ser谩n los primeros en aparecer en el Stream. Es por ello que la operaci贸n es ligera cuando el Stream es secuencial o se us贸 la operaci贸n unordered() (no disponible en todos los Streams, ya que la operaci贸n pertenece a otra clase).

Como reto adicional, crea el c贸digo para representar lo que hace la operaci贸n limit.

peek

peek funciona como una lupa, como un momento de observaci贸n de lo que est谩 pasando en el Stream. Lo que hace esta operaci贸n es tomar un Consumer, pasar los datos conforme van estando presentes en el Stream y generar un nuevo Stream id茅ntico para poder seguir operando.

La estructura de peek es simple:

Stream peek(Consumersuper T> consumer)

Usarlo puede ayudarnos a generar logs o registros de los datos del Stream, por ejemplo:

Stream serverConnections =
    server.getConnectionsStream()
        .peek(connection -> logConnection(connection, new Date()))
        .filter(鈥)
        .map(鈥)
    //Otras operaciones鈥

skip

Esta operaci贸n es contraria a limit(). Mientras limit() reduce los elementos presentes en el Stream a un numero especifico, skip descarta los primeros n elementos y genera un Stream con los elementos restantes en el Stream.

Esto es:

Stream first10Numbers = Stream.of(0,1,2,3,4,5,6,7,8,9);
Stream last7Numbers = first10Numbers.skip(3); // 3,4,5,6,7,8,9

Esto puede ser de utilidad si sabemos qu茅 parte de la informaci贸n puede ser descartable. Por ejemplo, descartar la primera l铆nea de un XML (), descartar metadatos de una foto, usuarios de prueba al inicio de una base de datos, el intro de un video, etc.

sorted

La operaci贸n sorted() requiere que los elementos presentes en el Stream implementen la interfaz Comparable para poder hacer un ordenamiento de manera natural dentro del Stream. El Stream resultante contiene todos los elementos pero ya ordenados, hacer un ordenamiento tiene muchas ventajas, te recomiendo los cursos de algoritmos de Platzi para poder conocer a mas detalle estas ventajas.

Conclusiones

Las operaciones intermedias nos permiten tener control sobre los streams y manipular sus contenidos de manera sencilla sin preocuparnos realmente por c贸mo se realizan los cambios.

Recuerda que las operaciones intermedias tienen la funcionalidad de generar nuevos streams que podremos dar como resultado para que otras partes del c贸digo los puedan utilizar.

Aunque existen otras operaciones intermedias en diferentes implementaciones de Stream, las que aqu铆 listamos est谩n presentes en la interfaz base, por lo que entender estas operaciones te facilitara la vida en la mayor铆a de los usos de Stream.

Aportes 15

Preguntas 3

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesi贸n.

En el caso de flatMap, yo lo veo un poco m谩s claro de esta forma:

    List<Email> allEmailsToNotify = getPlatziUsers()
      .stream()
      .filter(PlatziStudent::isEmailSubscribed)
      .map(PlatziStudent::getEmails)
      .flatMap(Collection::stream)
      .collect(Collectors.toList());

Se nota un poco mejor que el Stream<Stream<Email>> se convierte en Stream<Email>, que finalmente se transforma en List<Email>.

Los que tienen dudas a煤n sobre el uso de flatMap con este articulo pueden aclarar las dudas.

Con respecto al hacer _chaining _de operaciones filter, si bien facilita su legibilidad pero me parece que afecta el performance. Ya que, si tenemos un stream de 1000 cursos por ejemplo, al hacer:

courses.filter(course -> course.getName().contains("Java"))
    .filter(course -> course.getDuration() > 2.5)
    .filter(course -> course.getInstructor().getName() == Instructors.SINUHE_JAIME)

en el peor de los casos (cuando ninguna condici贸n se cumpla) por cada .filter estar铆amos haciendo 1000 iteraciones quedando en 3000 iteraciones en total. En cambio si hacemos algo como:

courses.filter(course -> course.getName().contains("Java") && course.getDuration() > 2.5 && course.getInstructor().getName() == Instructors.SINUHE_JAIME);

Solo serian 1000.

Por favor, corrijanme si me equivoco

No me quedo del todo claro el uso del flatMap, tal vez otro ejemplo me ayudar铆a.

Les dejo la documentaci贸n oficial de Stream por si est谩n interesados. Documentacion

Wow! Con skip() y limit() puedo hacer lo mismo que hago con PostgreSQL: https://www.postgresql.org/docs/8.1/queries-limit.html

Esto me ser谩 muy 煤til para la paginaci贸n de mis APIs: https://www.baeldung.com/rest-api-pagination-in-spring

Muchas gracias por la explicaci贸n de Operaciones Intermedias instructor Sinuh茅. Por cierto, respecto al .limit tengo entendido que se usar铆a de esta manera para establecer el nuevo rango de los elementos en un stream:

Stream<String> firstGeneration = pokemonNamesStream.limit(151);

https://www.arquitecturajava.com/java-8-flatmap/

un ejemplo para entender mas facilmente el flatMap

el ejemplo de flatMap
esta incompleto

creo que era map

public  Stream filter(Function mapper) {
    List mappedData = new LinkedList<>();
    for(T t : this.data){
        R r = mapper.apply(t);
        mappedData.add(r);
    }

    return mappedData.stream();
} 

Operaci贸n intermedia limit

public Stream limit(Long limit){
        List list = new LinkedList<>();
        for (int i = 0; i < limit; i++){
            list.add(this.data);
        }
        return list.stream();
    }

Implementaci贸n de limit:

class Nothing { 
      
     // Driver code 
     public static void main(String[] args){ 
           
         // list to save stream of strings 
         List<String> arr = new ArrayList<>(); 
           
         arr.add("geeks"); 
         arr.add("for"); 
         arr.add("geeks"); 
         arr.add("computer"); 
         arr.add("science"); 
          
         Stream<String> str = arr.stream(); 
           
         // calling function to limit the stream to range 3 
         Stream<String> lm = str.limit(3); 
         lm.forEach(System.out::println); 
     } 
 } 

Siempre buscar lo mas adecuado con estas nuevas herramientas que nos ofrece Java y utilizar e investigar antes de implementar las mismas formas de c贸digo de Java.

Genial

Algo que not茅 con Java 11 no s茅 si aplica en Java 8, es que el map, no se ejecuta hasta que el stream sea consumido por otra operaci贸n.