0% encontró este documento útil (0 votos)
33 vistas12 páginas

Uso de Lambdas en Programación Funcional

El documento explora el uso de expresiones lambda en la programación funcional, especialmente en el contexto de la programación orientada a objetos y su aplicación en lenguajes como Java y Scala. Se discuten las funciones de orden superior, el uso de lambdas con colecciones, y se compara la implementación de estas características en diferentes lenguajes. Además, se introduce el concepto de Option/Optional para manejar la presencia o ausencia de valores en colecciones.

Cargado por

Fran Mi
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
33 vistas12 páginas

Uso de Lambdas en Programación Funcional

El documento explora el uso de expresiones lambda en la programación funcional, especialmente en el contexto de la programación orientada a objetos y su aplicación en lenguajes como Java y Scala. Se discuten las funciones de orden superior, el uso de lambdas con colecciones, y se compara la implementación de estas características en diferentes lenguajes. Además, se introduce el concepto de Option/Optional para manejar la presencia o ausencia de valores en colecciones.

Cargado por

Fran Mi
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Paradigma Funcional en la

Programación Orientada a Objetos

Colecciones: Expresiones Lambda, Orden


Superior, Optionals

por Clara Allende


Junio 2019

Basado en Programación Funcional: Expresiones Lambda


por Fernando Dodino
Franco Bulgarelli
Matías Freyre
Carlos Lombardi
Nicolás Passerini
Daniel Solmirano
Versión 2.0
Marzo 2017

Distribuido bajo licencia C


​ reative Commons Share-a-like
Contenido

1 Introducción

2​ Uso efectivo de lambdas


2​.1 Lambdas con filter y map
2​.2 Consecuencias de las lambdas
3 Lambdas en otros Lenguajes

3.1 Scala

3.2 Java

4 ​(Una MUY breve explicación de) ​Option/Optional

2 de 12
DISCLAIMER​: Hay MUCHO que contar sobre las ideas de programación
funcional, en particular sobre lambdas y orden superior. En este apunte
queremos enfocarnos puntualmente en el uso de lambdas como argumentos de
las funciones de orden superior que usamos para manejar colecciones
(map/filter/etc) y cómo las usamos en la api de Streams; haciendo un contraste
con las implementaciones de otros lenguajes más funcionales que Java.

Si quieren saber más:


● Sobre aprender a programar en funcional: pueden meterse en M ​ umuki​.
● Sobre Haskell: chequear ​Learn You A Haskell (For Great Good)
● Sobre Scala: F
​ unctional Programming Principles in Scala
● Sobre Streams API: ​Stream in Java

1 Introducción
Las expresiones lambdas permiten definir ​funciones ​anónimas que se usan en
un contexto limitado (el de la misma función que estoy definiendo). Al no tener
nombre es una variante menos expresiva que una función cuadrado o suma,
que además se pueden utilizar en diferentes contextos. No obstante son una
herramienta muy útil, como veremos más adelante.

En H​ askell​ se codifica así:


\​x ​->​ x * x

Y también podemos definirla como:


cuadrado = ​\​x ​->​ x * x

La contrabarra (\) es el símbolo que remite a la letra griega lambda λ que como
habrán notado es el ícono de la programación funcional. Luego de los
parámetros que se separan por espacios, la flecha ​-> termina de definir el
cuerpo de la función.

Se evalúa de esta manera:


λ​ (​\​x ​->​ x * x) ​2
4

Otro ejemplo:
λ​ (​\​x y ​->​ x + y) ​2​ ​3
5

3 de 12
2 Uso efectivo de lambdas

2.1 Lambdas con filter y map


Las lambdas son útiles cuando queremos trabajar con funciones de orden
superior (funciones que reciben otras funciones como argumentos) y no
tenemos necesidad de reutilizar una expresión en otro contexto:

λ​ ​filter​ (​\​cliente ​->​ edad cliente > ​40​) clientes

λ​ ​map​ (​\​cliente ​->​ edad cliente) clientes

2.2 Consecuencias de las lambdas


Algo importante a tener en cuenta es que si no le damos un nombre a nuestras
funciones podríamos perder ​abstracciones útiles que podrían luego ser
utilizadas en otros puntos de nuestro programa, por lo tanto es importante ser
criteriosos respecto nombrar o no una función.

Por lo general, si tengo una forma sencilla de nombrar una determinada lógica
que forma parte de una función más grande, lo más probable es que no quiera
definir ese pedacito de lógica usando una lambda, sino con una función que se
llame como la idea que tenemos en la cabeza. Si no hay un nombre claro
asociado a ese pedacito de lógica, lo más probable es que no sea un concepto
del dominio que merezca la pena modelar como algo aparte.

4 de 12
3 Lambdas en otros Lenguajes

3.1 Scala
Scala y otros lenguajes multi-paradigma (que implementan ideas de más de un
paradigma a la vez, como JavaScript, Python, Kotlin, etc) tomaron estas idea de
funciones anónimas y las llevaron a la programación orientada a objetos.
Por ejemplo, las mismas funciones de antes en Scala se ven así:

3.2 Java
En Java todo era bastante más difícil, en particular las operaciones sobre las
colecciones eran bastante tediosas, ya que como no existía una abstracción
sobre cómo se recorre la colección
Por ejemplo, para obtener el subconjunto de clientes mayores de 40 años:

public List<Cliente>​ filterClientesMayores() {


​List<Cliente>​ clientesMayores = ​new​ ArrayList();

for (cliente: clientes) {


​ if (cliente.getEdad() > 40) ​ clientesMayores.add(cliente);
}
​return​ clientesMayores;
}

5 de 12
y si quiero los clientes menores a 21…

public List<Cliente>​ clientesMenoresA21() {


​List<Cliente>​ clientesMayores = ​new​ ArrayList();

for (cliente: clientes) {


​ if (cliente.getEdad() < 21) ​ clientesMayores.add(cliente);
}
​return​ clientesMayores;
}

Algo similar pasa si quiero por ejemplo quedarme con las edades de los clientes:

public List<​Integer​>​ mapEdadesClientes() {


​List<Integer>​ edades = ​new​ ArrayList();

for (cliente: clientes) {


​clientesMayores.add(cliente.getEdad());
}
​return​ edades;
}

Y si ahora quiero los nombres:

public List<​String​>​ mapNombresClientes() {


​List<String>​ nombres = ​new​ ArrayList();

for (cliente: clientes) {


​ clientesMayores.add(cliente.getNombre());
}
​return​ nombres;
}

Lo que está faltando ahí es una abstracción, algo que “encapsule” como se
recorre la colección para ver cuáles elementos cumplen (lo que hace f​ ilter​) o
para aplicarles una transformación (lo que hace ​map​); y poder pasar como
parámetro la función que quiero usar en cada caso...

Durante mucho tiempo, se desarrollaron librerías que “agregaban” este tipo de


abstracciones más funcionales a las colecciones de Java. Las más conocidas son
Google Guava (que agregaba otras ideas funcionales como ​Optionals​) y ​Apache
Commons. Sin embargo, ninguna era una solución completamente feliz, ya que

6 de 12
seguía siendo difícil leer el código, dado que el lenguaje no soportaba estas
abstracciones nativamente. Los mismos métodos en Guava se ven así:

public List<Cliente>​ filterClientesMayores() {


​return​ Iterables.filter(clientes, new Predicate<Cliente>() {
@Override
​public boolean​ apply(Cliente cliente) {
​return​ cliente.getEdad() > 40;
}​})
}

public List<Integer>​ mapEdadesClientes() {


​return Iterables.transform(clientes, new Function<Cliente,
Integer>() {
@Override
​public Integer apply​(Cliente cliente) {
​return​ cliente.getEdad();
})
}

Si bien representaba una mejora respecto a repetir los for, tiene la complejidad
de tener que conocer cómo funcionan Predicate y Function y es bastante
verboso. ApacheCommons funcionaba de manera similar…

Ahora bien, lo interesante es que estas abstracciones “Predicate” y “Function”


son lo que en Java se conoce como ​clases anónimas​, o sea, clases que creamos
ad-hoc, en el lugar que necesitamos, y no las usamos más. Esto es lo más
parecido que teníamos en Java a una función anónima o una lambda
expression… pero seguíamos sin tener nada ​propio del lenguaje que nos
permitiera hacer estas operaciones de manera menos compleja.

Hasta que en Java 8 finalmente escucharon a la comunidad, e introdujeron la api


de Streams.

public List<Cliente>​ filterClientesMayores() {


​return​ clientes.stream().filter(cliente -> cliente.getEdad() > 40)
.collect(Collectors.toList());
}

public List<Integer>​ mapEdadesClientes() {


​return​ clientes.stream().map(cliente -> cliente.getEdad());
}

7 de 12
¿Por qué tardaron tanto? Básicamente porque para que pudiésemos escribir
expresiones lambda, había que cambiar como el compilador hace la traducción
del código Java a código de máquina.

¿Qué es un Stream?
En programación funcional, las operaciones no tienen efecto de lado, porque los
objetos son inmutables: no se modifican. Entonces, las operaciones como map y
filter no cambian la colección original, sino que devuelven una nueva, del mismo
tipo, con el resultado de aplicar la operación a los elementos de la lista.
Eso permite “encadenarlas” de manera de formar una secuencia de pasos, algo
así como un “tubo” (​pipeline ​en inglés) por el que a medida que pasamos,
aplicamos una función al resultado del paso anterior:

En Java, la manera de modelar este pipeline es con Stream. Para mantener la


retrocompatibilidad con las versiones anteriores no modificaron las interfaces
de Collection, si no de generar un objeto intermedio, el stream, al que si le
definieron abstracciones funcionales. Por eso no es ​tan ​feliz como nos gustaría,
pero simplifica muchísimo las cosas.

8 de 12
Como “cultura general” les dejo una tabla que compara algunas operaciones
sobre colecciones entre Haskell, Java y Scala.
La nomenclatura es:
● f(x) es una transformación. Si el tipo de la colección es A, entonces el tipo
de f es f: A => B (donde B es otro tipo distinto)
● c(x) es un criterio, una condición. Si el tipo de la colección es A, entonces el
tipo de c es c: A => Boolean (es decir, devuelve true o false)

Operación Haskell Scala Java

aplicar una map (\x -> f(x)) lista lista.map(x => f(x)) lista.stream().map(x -> f(x))
transformación .collect(CollectorsToList());

obtener un filter (\x -> c(x)) lista lista.filter(x => c(x)) lista.stream().filter(x -> c(x))
subconjunto .collect(Collectors.toList());
según una
condición

saber si al menos any (\x -> c(x)) lista lista.exists(x => c(x)) lista.stream()
un elemento .anyMatch(x -> c(x));
cumple una
condición

saber si todos los all (\x -> c(x)) lista lista.forall(x => c(x)) lista.stream()
elementos .allMatch(x -> c(x))
cumplen con una
condición

reducción fold/foldl/foldr fold/reduce reduce


(obtener un
elemento a partir
de una lista)

9 de 12
4. (Una MUY breve explicación de) Option/Optional

Un tipo de operación que suele ser común cuando trabajamos con colecciones
es lo que en los lenguajes funcionales se conoce como ​find​: obtener un
elemento de una colección que cumpla una condición (cualquiera, generalmente
el primero que cumple).
Una manera fácil de implementar esta operación es usar filter:

Scala:
val clientes = List(new Cliente("Clari", 29), new Cliente("Leo",
33), new Cliente("Claudio", 60))

def unoMayorA20 = clientes.filter(cliente => cliente.edad > 20).head

Java:
public Cliente​ unoMayorA20() {
​return​ clientes.stream()
.filter(cliente -> cliente.edad > 20)
.collect(Collectors.toList())
.get(0);
}

(En Java no existe un método head, así que hacemos un get(0) que nos devuelve
el primer elemento de la colección).

Ambas implementaciones tienen el mismo problema: ¿qué pasa si la colección


está vacía? Lo mismo, explota (tira una excepción):

En Java lanza IndexOutOfBounds, porque estamos queriendo acceder a un


posición que no existe.

10 de 12
Entonces… el paradigma funcional tiene una abstracción para modelar estos
casos en los que el dato puede estar o no: en Haskell se llama Maybe, en Scala
se llama Option y en Java Optional. Y así está implementado find en los tres
lenguajes.

Volviendo al ejemplo:

Scala:

def unoMayorA20 = clientes.find(cliente => cliente.edad > 20)

Java:
public Optional<Cliente>​ unoMayorA20() {
​return​ clientes.stream()
.filter(cliente -> cliente.edad > 20)
.findFirst()
}

Detalles: en Java sólo existe findAny/findFirst; no hay ninguna versión que


permita parametrizar un criterio :(

Básicamente, en ambos casos el tipo Option/Optional modela un dato que


puede estar o no; por eso en ambos casos tenemos que hacer un get() del valor
contenido. Si no está presente, lanza una excepción (NoSuchElementException).

¿Por qué/para qué usar Optional?


● de alguna manera elimina la necesidad de chequear por null. El resultado
de la operación es de tipo Optional y lo puedo manejar
“polimórficamente”; ya que existen muchos métodos que están
preparados para hacer cosas siempre que haya algún valor presente.
● queda explícito en la firma del método/variable que el resultado de la
operación es algo que puede estar o no, lo cual nos permite tomar
decisiones al respecto.
● Las cosas que en Java devuelven null podemos modelarlas usando
Optional, permitiéndonos modelarlas con objetos (null no es un objeto,
por lo tanto, no le puedo mandar mensajes, por eso hay que hacer los
chequeos)

11 de 12
Desventajas:
● Como queda explícito en la firma, todos los métodos deben manejar el
hecho de que ese valor puede no estar, ya sea devolviendo Option, o
manejando explícitamente que pasa cuando no está.

Otras operaciones que devuelven Optional en la api de streams de Java son max
y min, que sirven para obtener el mayor/menor bajo un cierto criterio (una
relación de orden​):

clientes.stream()
.max(Comparator.comparing(cliente-> cliente.getEdad())).get();

Además, max y min necesitan como argumento una instancia de Comparator,


que es cómo se modela la relación de orden que usamos de criterio.

12 de 12

También podría gustarte