Composición de kleisli parte 2

August 26, 2019

...

Vamos a ver más ejemplos ahora con la composición du Kleisli. En este ejemplo vamos a tener una cadena de funciones construidas con promesas, donde el objetivo es actualizar un producto con una data estática siempre que el usuario sea el creador de ese producto, Por lo que vamos a tener dos documentos y también dos modelos. La api esta inspirada en Mongoose y, si te fijas, también hay muchos otros ORM o también ODM que implementan promesas, por lo que la mayoría de este código no deberías escribirlo, pero es para darte una idea de como implementaríamos las promesas en este caso:

const UserModel = ({
  findById: x => new Promise((res, rej) => {
    if(x == 1) {
      return res({ id: 1, name: 'Nicolas' })
    }

    rej('Not found')
  })
})

const ProductModel = ({
  find: query => new Promise((res, rej) => {
    if(query.name == 'Nintendo' && query.createdBy == 1) {
      return res({ id: 1, name: 'Nintendo', createdBy: 1 })
    }

    rej('Product Not found')
  }),
  update: (query, data) => new Promise((res, rej) => {
    if(query.name == 'Nintendo' && query.createdBy == 1) {
      return res({ ...data, id: 1 })
    }
    rej('Failed to update')
  })
})

Acá tenemos el modelo de usuario y también el modelo de producto, creamos dos métodos que simulan ir a una base de datos a actualizar, buscar por id y que también reciben una query.

Si te fijas todas estas devuelven una promesa y en todos los casos, cuando no se encuentre el elemento, rechazará la promesa con un mensaje de error.

Que rechace el error con un mensaje descriptivo es fundamental para el caso de manejar el error al final de nuestra operación ya que podremos decidir si le enviamos el error al usuario o si le enviamos algo más genérico, ademas nos será muy útil para poder depurar nuestra aplicación cuando esta este en producción y debamos ver los los.

Ahora vamos a la parte siguiente, como llamaríamos a estas promesas para:

  • Buscar el usuario si existe
  • Crear una query para ir a buscar el producto
  • Ir a buscar el producto
  • Si existe, actualizarlo con la data

El ejercicio es sencillo así que vamos a ver como lo podemos escribir:

UserModel.findById(1)
  .then(x => ({ name: 'Nintendo', createdBy: x.id }))
  .then(x => ProductModel.find(x))
  .then(x => ProductModel.update({ ...x }, { lala: 'lala' }))
  .then(console.log)
  .catch(console.log)

En algunos casos es mejor utilizar async/await, pero utilizaremos en este momento las promesas encadenadas con then para mostrar la composición de Kleisli más adelante.

Si se fijan el código es sencillo, busca por el id 1, crea un objeto, luego ese objeto se lo entrega al método find, luego lo que devuelve este se le entrega al método de update y finalmente actualizamos agregando la propiedad lala con el valor 'lala'.

Sin embargo este código tiene algo de duplicidad que, afortunadamente, hace mucho sentido de refactorizar ya que además nos ayudará en un futuro, vamos a empezar a hacer funciones que reciban el modelo y llamen a find, update y findById:

const findById = Model => id => Model.findById(id)
const find = Model => data => Model.find(data)
const update = (Model, data) => query => Model.update(query, data)

const createProductQuery = ({ id }) => Promise.resolve({
  createdBy: id,
  name: 'Nintendo',
})

Con esto, ya no es necesario ser tan explícitos para llamar a nuestras funciones en la cadena de then, al hacer que las funciones reciban primero el modelo y luego la data nos permite hacer nuestras funciones Point-free, por lo que ya no es necesario declarar funciones cuando llamamos a then, la última función se encarga de crear el objeto que utilizaremos para nuestra query, es importante que este también devuelva una promesa para que siempre se encuentre dentro de la cadena:

UserModel.findById(1)
  .then(createProductQuery)
  .then(find(ProductModel))
  .then(update(ProductModel, { lala: 'lala' }))
  .then(console.log)
  .catch(console.log)

Esto ya se ve un poco más limpio, eliminamos código repetitivo al sacar la declaración de funciones dentro de los then y también hicimos nuestras funciones mucho más generalizadas, por lo que estamos generando abstracciones en base a pequeñas modificaciones. Nuestro código esta más declarativo ya que ahora vemos el nombre de la función, sin tener que ver que está haciendo exactamente, ahora lo que podemos hacer es utilizar la composición de Kleisli para reducir aún más nuestro código repetitivo, en este caso, el then:

const composeM = method => (...ms) => (
  ms.reduce((f, g) => x => g(x)[method](f))
)

const composeP = composeM('then')

Esto que acabamos de crear recién es como nuestra función de compose, sin embargo esta irá llamando al método then de lo que sea que devuelva nuestra función, es por esto que es muy importante que nuestras funciones devuelvan siempre una promesa, de esta manera podemos componerlas utilizando ahora nuestra función de composeP:

const handler = composeP(
  update(ProductModel, { lala: 'lala' }),
  find(ProductModel),
  createProductQuery,
  findById(UserModel),
)

handler(1)
  .then(console.log)
  .catch(console.log)

Hemos separado los console.log del final por la sencilla razón de poder reutilizar esta lógica que hemos creado.

Este código quedo bastante corto y expresivo, no tenemos declaraciones de funciones ni tampoco el repetitivo then, este código se enfoca solo en lo que quiere hacer y no en como se hace, la parte imperativa la dejamos en nuestros primeros snippets por lo que cada vez que queramos componer software podemos utilizar esta estrategia para poder escribir menos y hacer más sin el uso de librerías pero conociendo nuestras leyes de la composición.

Cúal sería una abstracción valida para el ultimo código que escribimos de manera que puedas reutilizarlo más adelante?

Acá el código completo:

const composeM = method => (...ms) => (
  ms.reduce((f, g) => x => g(x)[method](f))
)

const UserModel = ({
  findById: x => new Promise((res, rej) => {
    if(x == 1) {
      return res({ id: 1, name: 'Nicolas' })
    }

    rej('Not found')
  })
})

const ProductModel = ({
  find: query => new Promise((res, rej) => {
    if(query.name == 'Nintendo' && query.createdBy == 1) {
      return res({ id: 1, name: 'Nintendo', createdBy: 1 })
    }

    rej('Product Not found')
  }),
  update: (query, data) => new Promise((res, rej) => {
    if(query.name == 'Nintendo' && query.createdBy == 1) {
      return res({ ...data, id: 1 })
    }
    rej('Failed to update')
  })
})


UserModel.findById(1)
  .then(x => ({ name: 'Nintendo', createdBy: x.id }))
  .then(x => ProductModel.find(x))
  .then(x => ProductModel.update({ ...x }, { lala: 'lala' }))
  .then(console.log)
  .catch(console.log)

const composeP = composeM('then')

const findById = Model => id => Model.findById(id)
const find = Model => data => Model.find(data)
const update = (Model, data) => query => Model.update(query, data)
const createProductQuery = ({ id }) => Promise.resolve({
  createdBy: id,
  name: 'Nintendo',
})

const handler = composeP(
  update(ProductModel, { lala: 'lala' }),
  find(ProductModel),
  createProductQuery,
  findById(UserModel),
)
handler(1)
  .then(console.log)
  .catch(console.log)

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.