Programación funcional con Javascript. (Parte VI)

 

¿Todavía conmigo? Bien! Espero que la entrada de hoy sea más amena y podamos poner en práctica algunos conceptos de los que hemos visto anteriormente.

¡Aviso! Vamos a tener que seguir con mente abierta. Entre el post anterior y éste, sé que habrá momentos en los que se piense que éstas son cosas sin sentido. Nada más lejos, todo empezará a tener sentido dentro poco.

Antes de empezar, y para los que quieran ir probando por su cuenta, usaremos Ramda-Fantasy, una implementación de la especificación que veíamos de Fantasy-Land, que en el momento de escribir esta serie aún estaba en una versión alpha.

Pero también hay otras alternativas Ramda-Fantasy como:

Sobre las estructuras que veíamos en el post anterior, hoy veremos unos tipos que las implementan y que nos ayudarán a solucionar problemas específicos que se repiten. ¡Vamos a ello!

 

Maybe

Maybe representa a un valor que puede que exista o no. En Java 8 se añadió este tipo, pero su nombre es Optional. Este tipo nos encajará en las ocasiones en las que esperamos un valor para una operación, pero algunas veces no está presente y no queremos realizar dicha operación. Yo identifico a Maybe en esos casos en los que, en programación imperativa, hay un valor que puede ser nulo y lo compruebo antes de hacer una acción.

Maybe puede tener dos valores realmente:

  • Nothing, para cuando el valor no existe.
  • Just, que va a contener el valor cuando sí que existe.
const { Nothing, Just } = Maybe
Nothing() // Maybe.Nothing()
Just(1) // Maybe.Just(1)

 

Hasta aquí todo bien, pero de poca utilidad. Vamos a ver cómo se comportan. Según la especificación de Fantasy-Land, los Maybe, entre otras cosas, deben implementar la especificación de Functor. En la tabla que aparece en la web de Ramda-Fantasy vemos que ésto se cumple y que Maybe es un Monad, un Applicative y también un Functor (además de otros tipos).

 2-2

 

Con ésto, ya sabemos cómo interactuar con Functors y Monads. Vamos a probarlo:

const multiplyBy2 = x => x * 2
const justPlus3 = x => Just(x + 3)

Nothing()
.map(multiplyBy2) // Maybe.Nothing()
.chain(justPlus3) // Maybe.Nothing()

Just(1)
.map(multiplyBy2) // Maybe.Just(2)
.chain(justPlus3) // Maybe.Just(5)

Como se puede ver, cualquier operación sobre Nothing nos devuelve de nuevo Nothing; en cambio, sobre Just va aplicando las transformaciones necesarias.

Vamos a ponerlo de otra manera y ver un ejemplo diferente:

const maybeAValue = x => x === null ? Nothing() : Just(x)

const doSomeCalculation = compose(chain(justPlus3),
								map(multiplyBy2),
                                maybeAValue)
                                
doSomeCalculation(null) // Maybe.Nothing()
doSomeCalculation(1) // Maybe.Just(5)

 

Se ejecuta según lo esperado y vemos que, además, podemos usar compose (recordad, de derecha a izquierda o, en este caso, de abajo a arriba) para crear el flujo y que vuelva a ser Point-free.

En el ejemplo, arriba del todo, vemos que somos nosotros, los desarrolladores, quienes definimos qué es Nothing y qué es Just.

Suele haber una confusión con este punto. Puede haber casos donde Just(null) es totalmente válido y se realicen operaciones sobre esa estructura. Quizás podríamos hacerlo de otra manera para validar que los valores que le pasemos solo sean números. 

const maybeAValue = x => typeof x !== Number ? Nothing() : Maybe.of(x)
// Maybe.of(x) == Just(x)

Ahora un nombre más apropiado para la función sería maybeANumber. Destacar también que estamos usando Maybe.of y no Just para inicializar nuestro Maybe (en caso de que sea un número). En este caso, es lo mismo llamar a uno que a otro.

doSomeCalculation(null) // Maybe.Nothing()
doSomeCalculation(undefined) // Maybe.Nothing()
doSomeCalculation('abc') // Maybe.Nothing()
doSomeCalculation({ 1: 'abc' }) // Maybe.Nothing()
doSomeCalculation(5) // Maybe.Just(13)

 

Con Maybe no tendremos que hacer validaciones de tipo o de contenido más allá del momento de creación del propio contenedor Maybe, y definiendo en un único punto qué es válido y qué no. ¿Y si las librerías devolviesen ese tipo de contenedores? Solo para centrarse en qué hacer cuando el valor es el esperado y descartar todos los demás.

Either

Either, de un modo similar a cómo hacíamos con Maybe, nos sirve para indicar que puede darse uno de dos valores que no tiene por qué ser ninguno de los dos un error, pero suele ser lo más extendido para su uso. Por ejemplo, si en la función esperamos como input una dirección de email y nos envían un String que no cumple esa condición, podremos indicar que ha ocurrido un error. Vamos a verlo con un ejemplo.

Primero unas funciones no relacionadas con Either que usaré para el ejemplo, de modo que podamos hacer algo más útil.

const matchEmailAddress = match(/^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/)
const isValidEmail = compose(not, isEmpty, matchEmailAddress)
const capitalize = w => toUpper(head(w)) + toLower(tail(w))
const showGreetings = name => `Hello ${name}`
const doSomeCoolStuffHere = compose(showGreetings, capitalize, head, split(@))

 

Either tiene dos subtipos, al igual que Maybe: Nothing y Just, que son Left y Right.

const { Left, Right } = Either

 

Left se suele usar para indicar el error, aunque como digo no tiene por qué, y Right del caso contrario. Vamos a crear una función que nos devuelva Left o Right según la validez de la dirección de email que se envíe por parámetros:

const eitherAnEmail = address => isValidEmail(address) ? Right(address)
											           : Left(`${address} is not a valid email address`

Como se puede ver, devolvemos Right (o Either.of) con la dirección si es válida, y si no un mensaje en el que decimos que no lo es. Como ya podéis suponer Either es un Functor, un Applicative y también un Monad.

 

10

Así que ya sabemos cómo trabajar con él:

eitherAnEmail('arthur@company.com').map(identity)
// Either.Right('Hello Arthur')
eitherAnEmail('not@valid')
// Either.Left('not@valid is not a valid email address')

Aquí debería surgir alguna duda como "¿No es ésto mismo lo que hace Maybe?" ó "¿Cómo trabajo con el contenido de Left si no puedo usar map?" Pues a diferencia de Maybe, en Either tenemos un método llamado either (a la que se ha aplicado curry) que recibe dos funciones: una para los casos de Left, otra para los Right, y un último parámetro que es el propio Either a evaluar.

const handleEmailInput = Either.either(err => console.error(error), doSomeCoolStuffHere)
handleInput(eitherAnEmail('arthur@company.com'))

Podríamos reordenar el flujo de otra manera:

const showError = error => `D'Oh! ${error}`
const showGreetings = name => `Hello ${name}`

const handleEmailInput = Either.either(showError, showGreeting)

const extractNameFromEmail = map(compose(capitalize, head, split('@')))

const doSomeCoolStuffHere = compose(handleEmailInput, extractNameFromEmail, eitherAnEmail)

doSomeCoolStuffHere('arthur@company.com')
// "Hello Arthur"
doSomeCoolStuffHere('arthur@company')
// "D'Oh arthur@company is not a valid email address"

Ahora tenemos dos funciones: una para manejar el error y otra en caso de que todo haya ido bien. Pasamos ambas a handleEmailInput para que llegado el momento ejecute una u otra. Las funciones que realizan operaciones con Right las hemos movido y, ya que todas ellas recibirían un Either, aplicamos map y luego compose para realizar todas las operaciones; y llamamos a esta función extractNameFromEmail. Finalmente la función doSomeCoolStuffHere valida el input con eitherAnEmail, le pasa el resultado a extractNameFromEmail para las transformaciones y delega en handleEmailInput para que se encargue del Right o Left según sea necesario. Al final, ejecutándolo, vemos que para cada uno de los casos se obtiene el resultado esperado.

Folktale tiene además una estructura similar, llamada Validation, que nos sirve para acumular varios errores que se quieren pasar al usuario en vez de uno solo, como por ejemplo si quisieses compartir con el usuario que el password introducido no tiene la longitud adecuada, no contiene mayúsculas y por lo menos dos dígitos en medio del mismo.

Similar a Maybe, solo nos centraremos en las transformaciones del valor como si siempre fuese a ser el adecuado. Finalmente realizaremos una acción si era el valor esperado y otra como si no lo fuese.

Future

Ya hemos visto cómo lidiar con casos en los que no existe el valor o no es el esperado. Ahora veremos cómo lidiar con valores que provienen de tareas asíncronas como lecturas de ficheros, acceso a base de datos, peticiones http a servidores o cualquier otra que signifique que no tengamos el resultado al momento.

Veamos cómo creamos un Future:

Future((reject, resolve) => {
	setTimeout(() => {
    	try {
        	// Do some async stuff here
            resolve(someValue)
        } catch(error) {
        	// Something went wrong
            reject(error)
        }
    }, 1000)
})

Future recibe una función con dos argumentos: Reject y Resolve. Con reject, devolveremos un error y con resolve, el resultado de la tarea asíncrona. Veamos un ejemplo siguiendo más o menos esa estructura:

const futureAction = success => Future((reject, resolve) =>
									setTimeout(() => !success ? reject('Something wrong happened in Future')
                                                              : resolve('Everything worked as expected in Future'), 1000))

 

En este ejemplo tenemos una función a la que pasamos un argumento y que devuelve un Future, que a su vez ejecuta una tarea asíncrona (setTimeout) y, si el argumento que le hemos pasado es Falsy, ejecutara reject, pero si es Truthy ejecutará resolve.

Como ya os podéis imaginar, Future es un Functor, un Monad y un Applicative.

 

16

Por lo que podemos trabajar con él a través de map, chain o ap, entre otros, y solo afectarán a los valores devueltos en resolve y no así en reject.

const futureSuccess = futureAction(true)

futureSuccess
.map(concat(__, '!!!'))

Se asemeja en estructura a las Promesas de ES6: ambos reciben resolve y rejec, y se puede “mapear” sobre ellos (en caso de las promesas con then). Pero, a diferencia de las promesas, Future no se ejecutará al instante. Las promesas son Eager, se ejecutan al instante, pero Future es Lazy, no se ejecuta hasta que se invoque el método fork. Veámoslo:

const futureAction = success => Future((reject, resolve) => 
									setTimeout(() => !success ? reject('Something went wrong in Future')
                                                           : resolve('Everything worked as expected in Future'), 1000))
                                                                                                              
const es6Promise = success => new Promise((resolve, reject) => 
										setTimeout(() => !success ? reject('Something wrong happened in Promsie')
                                        					   : resolve('Everything worked as expected in Promise'), 1000))
                                                                  
const futureSuccess = futureAction(true) // Future not executed yet

const promiseSuccess => es6Promise(true) // Promise executed right now

// ... other statements in between

futureSuccess
.fork(err => console.error(`D'Oh! ${error}`),
	success => console.log(`Hurray! ${success}`))
// Executed right now: Hurray! Everything worked as expected in Future

promiseSuccess
.then(success => console.log(`Hurray! ${success}`),
		error => console.error(`D'Oh! ${error}`))
// Already execute: Hurray! Everything worked as expected in Promise

En el momento en el que llamamos a la función es6Promise, la promesa empieza a ejecutarse, mientras que Future esperará a que fork sea invocado.

Veamos ahora cómo, por ejemplo, map solo afecta a la rama de valores que van por resolve:

futureAction(true)
.map(concat(__, '!!!'))
.fork(error => console.error(`D'Oh! ${error}`),
	success => console.log(`Hurray! ${success}`))
// Hurray! Everything worked as expected in Future!!!

Ahora el problema sería, en este punto, que tendríamos que crear siempre un wrapper con Future sobre las promesas o funciones con tareas asíncronas que usen callbacks para trabajar con Future. Vamos a verlo: 

const futureArtists = artist => 
	new Future((reject, resolve) => 
    		fetch(`http://getsomeartisturl.io?q=${artist}`)
            .then(resolve, reject))
            
const readFile = file => new Future((reject, resolve) => 
	fs.readFile(file, 'utf-8', (err, data) => err ? reject(err) : resolve(data)))

No resulta demasiado cómodo, pero en este caso podemos usar la librería Futurize que nos servirá para conseguir que Future actúe como wrapper de funciones que trabajan con el patrón de node (err, data) o que devuelven promesas.

import { futurize, futurizeP } from 'futurize'

const futureCb = futurize(Future)(callbackStyleFunction)
const futurePromise = futurizeP(Future)(promiseBasedFunction)

Finalmente veamos ya un ejemplo con todo. En esta ocasión usaré axios como cliente HTTP para hacer la request de los datos:

const futureArtist = futurizeP(Future)(artist => axios({
	url: 'https://api.spotify.com/v1/search',
    method: 'get',
    params: {
    	q: artist,
        type: 'artist'
    }
}))

const displayErrorMessage = error => console.error(`D'Oh! ${error}`)
const displayArtistData = artists => console.log(`Success! ${artists}`)

futureArtist('red hot chili peppers')
.map(path(['data', 'artists', 'items'])) // Parse response
.map(compose(prop('name'), head)) // Pick first artist name
.fork(displayErrorMessage, displayArtistData)

Envolvemos la función que recupera los datos con futurizeP, ya que es una función que devuelve una promesa, a continuación la invocamos pasándole el argumento artista, le aplicamos las transformaciones que deseamos y finalmente invocamos el método fork que ejecuta la request en ese momento y se encarga de llamar a la función que nos mostrará el error o los datos.

Y aquí el resultado:

// Success! Red Hot Chili Peppers

Me gustaría añadir un par de detalles más como que en la librería de Folktale este tipo se llama Task, pero su finalidad es la misma y que en Ramda tenemos las funciones composeP o pipeP como otras alternativas para trabajar con promesas.

 

* Folktale - Task

* Mdn - Falsy

* Mdn - Truthy

* Mdn - Promise

* Futurize

* Axios

* Ramda - composeP

* Ramda - pipeP

 

IO

Éste será el último tipo que veremos. Con él aislaremos la operaciones que generan efectos secundarios (side effects). Con ésto deberíamos cerrar todos los puntos que habíamos dejado pendientes de post previos en esta serie.

El constructor del tipo IO recibe una función sin argumentos.

IO(() => console.log('Some log message'))

Una manera de que no dependan de datos estáticos y, aun así, cumplamos las directrices que venimos siguiendo creando IO dentro de una función y creando de esta manera un closure, de modo que cuando se ejecute tenga acceso a las variables usadas en su inicialización.

const ioLogger = message => IO(() => console.log(message))

Hasta aquí todo bien, pero al ejecutar el ejemplo de arriba vemos que ioLogger nos devuelve el objeto IO y no ejecuta su contenido. Para ejecutarlo deberemos llamar a la función runIO.

ioLogger('some important message here')
.runIO() // Shows 'some importart message here'

Como os podéis imaginar IO implementa map, chain y ap.

27

Así que con esos métodos podríamos trabajar con los resultados que devuelva la función interna. Eso sí, recordad que no se ejecutará hasta llamar a runIO.

IO(() => document.querySelectorAll('#wiki-list > table > tbody > tr'))
.map(pluck('cells'))
.map(map(compose(prop('textContent'), nth(1))))
.map(tap(console.log))
.runIO()

 29

30

Un último ejemplo con un par de funciones con IO:

// Utils
const pickArtistItems = path(['data', 'artists', 'items'])
const projectArtistsFields = project(['name', 'href', 'genres', 'popularity'])
const filterByPopularity = filter(artist => artist.popularity > 70)

// IO
const ioPrintArtists = artists => IO(() => artists
	.map(({ name, genres, popularity }) => document.createTextNode(`${name} - ${genres[0]} - ${popularity}`))
    .forEach(node => {
    	const li = document.createElement('li')
        li.appendChild(node)
        document.getElementById('artistList').appendChild(li)
    }))
    
const ioPrintError = error => IO(() => document.getElementById('errors').innerHTML = `Something bad happened: ${error}`)

const runIO = fn => compose(invoker(0, 'runIO'), fn)

// Future 
const futureArtists = futurizeP(Future)(artist => axios.get(`https://api.spotify.com/v1/search?type=artist&q=${artist}`))
const displayFutureResults = future => future.fork(runIO(ioPrintError), runIO(ioPrintArtists))

// Main flow
const parseArtistsData = map(compose(filteryByPopularity, projectArtistsField, pickArtistItems))
const searchArtists = compose(displayFutureResult, parseArtistsData, futureArtists)

//Run it!
searchArtist('doors')

En el ejemplo usamos la API de Spotify para recuperar una lista de artistas con una popularidad de más de 70 puntos y los añadimos a una lista en una página web o volcamos el error en un div. Aquí los resultados para cada uno de los casos.

results1

error1

Si bien la llamada HTTP podría estar contenida en un IO, no he visto casos en los que se mezcle Future con IO. A mi entender ya está dentro de un contenedor y aunque no hay un método runIO que indique que se va a realizar una operación de input/output, forzamos su llamada con fork por lo que debería ser suficiente. Encontré un ejemplo donde usan Lazy-Either para una operación similar y se asemeja más a un Future que a un IO.

* Lazy-Either

Finalmente repasar otro de los conceptos que ya habíamos comentado, la mayoría de funciones serán puras y moveremos los efectos secundarios (side effects) a los extremos. De este modo reduciremos los errores y nos será más sencillo encontrar bugs y realizar los tests.

Type Signatures

Un tema que no he cubierto en esta serie y que quizás os haya llamado la atención a la hora de revisar documentaciones como la de Ramda son las firmas que aparecen en las funciones. Os dejo aquí un enlace explicativo sobre el tema ya que es algo muy usado en la comunidad de Programación Funcional y sería conveniente echarle un vistazo.

Cierre

Algo que no he comentado demasiado durante la serie es que, si echáis un vistazo atrás, serán pocos o ninguno los if/else (sí que hay operadores ternarios) y creo que ningún for/while los que podréis encontrar en los ejemplos que hemos visto durante toda la serie. Esto es algo de lo que comentábamos al principio. Unido a que en muchos puntos no nos centramos en pasar variables de un punto a otro, nuestro código se centra más en qué cosas hacer y no en cómo se deben hacer. Sin duda, ciertas expresiones las seguiremos necesitando, pero vemos que se pueden reducir en gran medida.

Como reflexión final a lo visto en toda la serie, os aconsejaría que empezaseis poco a poco y solo uséis aquello que os parezca práctico. Si solo os convence la parte que veíamos de los métodos de Array, o la parte que usaba solo Ramda, si solo queréis usar funciones puras o Curry, adelante. Ya que JS es multiparadigma, podéis usar aquello que os resulte realmente útil de cada mundo. No intentéis cubrir de primeras todo lo visto aquí, id poco a poco con pequeños ejemplos o pequeñas funcionalidades. Requiere práctica y seguro que si vais probando por vuestra cuenta reforzaréis y mejoraréis las nociones vistas en esta serie.

Con ésto doy por cerrada la serie de Programación Funcional con JavaScript.

Espero que os haya resultado interesante, que podáis sacarle partido en algún proyecto, os pueda servir para comprender alguna de las herramientas que hoy en día usan alguno de estos conceptos o, por qué no, os de pie para introduciros al mundo de la programación funcional y que lo uséis para aprender otros lenguajes como Haskell, Scala, F# o Elm.

 

Enlaces de referencia

Os dejo aquí algunos de mis enlaces favoritos relacionados con la Programación Funcional, de modo que podais seguir investigando por vuestra cuenta.

https://drboolean.gitbooks.io/mostly-adequate-guide/

https://leanpub.com/fljs/ - https://github.com/getify/Functional-Light-JS

https://medium.com/javascript-scene

http://www.tomharding.me/ 

https://medium.com/javascript-scene/master-the-javascript-interview-what-is-functional-programming-7f218c68b3a0

https://github.com/jaysoo/example-fp-youtube-search

https://www.youtube.com/watch?v=SfWR3dKnFIo

https://www.youtube.com/watch?v=m3svKOdZijA

https://www.youtube.com/watch?v=7Zlp9rKHGD4

https://www.youtube.com/watch?v=myISHtMMeyU

https://www.youtube.com/watch?v=ZQSU4geXAxM

http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

https://egghead.io/courses/professor-frisby-introduces-composable-functional-javascript?utm_content=buffer03cfd&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer

Temas: Programacion Funcional, javascript, future, futurize, maybe, either, io

Acerca de The Softtek Blog

The Softtek Blog proporciona perspectivas y conocimientos prácticos acerca de la tecnología digital y las tendencias que hoy tienen impacto en todas las industrias.

Publicaciones recientes

Publicaciones por tema

ver todos
To top