Composición de funciones

August 13, 2019

...

Composición

En palabras simples la composición es el arte de juntar pequeñas piezas, fáciles de entender y de identificar para armar una pieza más grande. Podemos pensar en la composición cómo armar una estructura en base a ladrillos o incluso o piezas de lego. Si esto lo llevamos a un sistema informático podríamos pensar en los módulos o los componentes de react. Estas son pequeñas piezas reutilizables que nos permiten construir una aplicación más grande. Estos componentes o funciones pequeñas por si solas no tienen mucho sentido a menos que se conecten con más partes de nuestra aplicación. Y esas partes tampoco hacen sentido a menos que se conecten con otras partes.

Composición de funciones

La Composición de funciones esta dada de manera de que:

const y = f(x)
const z = g(y)

z == g(f(x))

O también:

Si existe una función F que reciba A y devuelva B:
F :: A -> B

Y también existe una función G que recibe B y devuelve C:
G :: B -> C

Entonces existe una función H que recibe A y devuelve C
H :: A -> C

Y esta función H es igual a la composición de F con G
H :: F . G

Que estrictamente seria esto:

H(x) === F(G(x))

Esto quiere decir que, si existen dos funciones donde el tipo de salida de uno, es el mismo que el tipo de entrada de otra, estas se pueden transformar en una única función.

Ahora vamos a ver como podemos escribir esto en javascript:

compose(g, f) === x => g(f(x))
compose(a, b, c, d) === x => a(b(c(d(x))))

Y acá la implementación de la función de compose:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x)

Como ven la implementación de compose tomará todos los argumentos que le pasemos, que en este caso es fns y los llevará a un arreglo, la sintaxis de los 3 puntos que aparece en la definición de la función (...fns) tomará todos los argumentos que le estemos pasando a la función y los transformará en un arreglo, el cual luego podemos referenciar utilizando fns, luego compose devolverá otra función que recibe un argumento x, este argumento es el que le pasará a la última función que le pasemos como argumento a compose, a sea que si:

compose(a, b, c)(x)

x se le pasará a c, el resultado de c se le pasará como argumento a b y el resultado de b se le pasará como argumento a a.

Por lo que si queremos definir una composición de funciones que se quede esperando un argumento para luego ejecutarla en un futuro podemos hacer lo siguiente:

const f = compose(a, b, c)

f(x)

Porque esto es útil? Porque podemos combinar pequeñas funciones que ya hemos creado para construir funciones más grandes, porque podemos reutilizar todo es código posible, y porque es mejor la versión de compose por sobre la anterior?, porque es más fácil de leer!, vas de derecha a izquierda sin ver paréntesis ni argumentos, solo ves comportamiento, no ves datos!, y mencioné que es Point Free? Si no sabes que es Point free puedes ver mi artículo de programación tácita donde vemos este tema.

Nuestra función de compose for ahora solo es teórica, de hecho esta existe en nuestro buen amigo Haskell y la notación es solo con un punto:

f . g

Pero nosotros para referirnos a composición de funciones utilizaremos compose.

Vamos a ver ahora un ejemplo de programación imperativa:

const users = [
  { id: 1, nombre: 'nicolas', apellido: 'schurmann' }
]

const getNombreCompleto = (_users) => {
  const primero = _users[0]
  const capitalizados = {
    nombre: _.upperFirst(primero.nombre),
    apellido: _.upperFirst(primero.apellido),
  }
  return `${capitalizados.nombre} ${capitalizados.apellido}`
}

Wow, tan imperativo, que verboso.

Algunos desarrolladores les gusta este estilo porque pueden ver la implementación en la misma función sin tener que saltar a otro archivo o a otra función para ver que es lo que esta sucediendo. Y está bien, después de todo es una preferencia. Sin embargo este código es propenso a tener errores ya que los modelos pueden cambiar, además de que la cantidad de código que debes leer es bastante para una implementación sencilla. Lo que hace esta función es:

  1. Toma el primer elemento
  2. Capitaliza las propiedades nombre y apellido
  3. Le da formato al nombre completo

Vamos a reescribir esto con funciones que luego podremos reutilizar y crearemos una composición a la antigua, a sea, con f(g(x)).

const users = [
  { id: 1, nombre: 'nicolas', apellido: 'schurmann' }
]
const head = x => x[0]
const capitalizaNombreYApellido = x => ({
  nombre: _.upperFirst(x.nombre),
  apellido: _.upperFirst(x.apellido),
})
const nombreCompleto = x => `${x.nombre} ${x.apellido}`

const getNombreCompleto = (_users) =>
    nombreCompleto(capitalizaNombreYApellido(head(_users)))

Esto queda bastante más conciso, ahora es una función de una sola línea donde estamos definiendo que queremos hacer pero no como queremos hacerlo, el como lo dejamos para la definición de las funciones. Sin embargo esto queda un poco más difícil de leer, por lo que vamos a reescribirla utilizando compose:

const users = [
  { id: 1, nombre: 'nicolas', apellido: 'schurmann' }
]
const head = x => x[0]
const capitalizaNombreYApellido = x => ({
  nombre: _.upperFirst(x.nombre),
  apellido: _.upperFirst(x.apellido),
})
const nombreCompleto = x => `${x.nombre} ${x.apellido}`

const getNombreCompleto = (_users) => {
  return compose(
    nombreCompleto,
    capitalizaNombreYApellido,
    head,
  )(_users)
}

Ya estamos casi, ahora reescribimos la función para que haga uso de la composición de funciones, vamos a volverla Point free a esta también y quitar el return:

const users = [
  { id: 1, nombre: 'nicolas', apellido: 'schurmann' }
]
const head = x => x[0]
const capitalizaNombreYApellido = x => ({
  nombre: _.upperFirst(x.nombre),
  apellido: _.upperFirst(x.apellido),
})
const nombreCompleto = x => `${x.nombre} ${x.apellido}`

const getNombreCompleto = compose(
  nombreCompleto,
  capitalizaNombreYApellido,
  head,
)

Miren como quedo finalmente nuestra función getNombreCompleto, ahora se lee desde abajo hacia arriba:

  1. Toma el primer elemento
  2. Capitaliza el nombre y apellido
  3. Luego obtiene el nombre completo

No estamos viendo ningún detalle de la implementación, esta se encuentra detrás de las funciones que extrajimos y volvimos componibles. Este formato, cuando no vemos la implementación si no solo el comportamiento se llama programación declarativa. Veremos este tema de la programación declarativa en profundidad más adelante y con ejemplos, pero por ahora nos limitaremos a la composición de funciones.

Ahora nos queda mover las funciones que creamos a nuestra librería de utils y nos quedaría algo más o menos así:

import { head, capitalizaNombreYApellido, nombreCompleto } from './utils'

const users = [
  { id: 1, nombre: 'nicolas', apellido: 'schurmann' }
]

const getNombreCompleto = compose(
  nombreCompleto,
  capitalizaNombreYApellido,
  head,
)

Porque esto es útil?, porque no tenemos que ver el detalle del código, esto nos permite ver que queremos realizar en el código a nivel de la lógica, no nos interesa ver el detalle en este momento para encontrar un error si no en que es lo que se está intentando hacer. Por lo general los errores que ocurren cuando estamos desarrollando son debido a problemas en la lógica y no en sintaxis, esto es debido a que los seres humanos somos malos programando a diferencia de las máquinas. Ademas la definición de nuestras funciones queda bastante explícita, sabemos inmediatamente que están haciendo ya que son solo funciones de una línea en su mayoría.

Espero que este post te haya gustado, puedes suscribirte en la cajita más abajo, recuerda seguirme en youtube y en twitter, los links más abajo en el párrafo donde sale mi foto.


Suscríbete

Suscríbete a la lista para más cursos, posts y videos tutoriales. Prometo no enviarte más de un correo semanal 🙏

Creado por Nicolás Schürmann ingeniero e instructor de software. Cuando no está programando, esta frente a una cámara dictando cursos, creyéndose youtuber o apoyando a sus alumnos. Puedes seguirlo en twitter o también suscribirte a su canal de youtube. Considera comprar sus cursos por este medio y así apoyas al instructor.