Curry…en JS?

Currying es una técnica de programación que involucra utilizar una función que acepta n parametros para crear otras funciones que utilizan esa función como base.

Decirlo es fácil pero para entenderlo bien veamos un ejemplo de una función que no utiliza currying y transformemosla para utilizar esta técnica.

Sin Currying

Supon que tienes una función que se encarga del proceso de registro de usuarios en tu sitio:

const registeredUsersDB = []

function registerUser(userType = 'default', newUser) {

    const userPermissions = {
        dashboard: [],
        users: [],
        dataAnalysis: []
    }

    if (userType === 'default') {
        userPermissions.dashboard = ['r', 'w']
    }
    if (userType === 'analyst') {
        userPermissions.dashboard = ['r', 'w']
        userPermissions.dataAnalysis = ['r']
    }
    
    if (userType === 'admin') {
        userPermissions.dashboard = ['r', 'w']
        userPermissions.dataAnalysis = ['r']
        userPermissions.users = ['r', 'w']
    }

    const newUserObject = {
        user: newUser,
        permissions: userPermissions
    }

    registeredUsersDB.push(newUserObject)

    return newUserObject
        
}

La función acepta 2 parámetros:

  • newUser es el objeto con los datos del usuario a registrar (nombre completo, nombre de usuario, etc)
  • userType con 1 de 3 tipos de usuarios posibles:
    • default
    • analyst
    • admin

Dependiendo del tipo de usuario se habilitan accessos de lectura (read) y de escritura (write) a diferentes secciones de la app. Esto se guarda en el arreglo de userPermissions.

Finalmente registeredUsersDB guarda el nuevo usuario. Una app real tendría un proceso de llamado a un API o una BD aquí, pero nosotros simulamos eso con un arreglo.

Si queremos registrar un usuario de cada tipo podemos hacer lo siguiente:

registerUser('analyst', {id: 1, userName: 'Solid'})
registerUser('default', {id: 2, userName: 'Liquid'})
registerUser('admin', {id: 3, userName: 'Solidus'})

*Si sabes de que videojuego son las referencias, ya eres mi amigo.

Nada fuera de lo normal. ¿Qué tanto podría cambiar nuestra función si la convertimos para utilizar currying?

Con Currying

Para utilizar currying necesitamos cambiar las entradas y las salidas de nuestra función de manera que en vez de llamar a registerUser('analyst', userObject) llamemos a registerUser('analyst')(userObject). ¿Cómo se hace eso?

const registeredUsersDB = []

function registerUser(userType = 'default') {

    const userPermissions = {
        dashboard: [],
        users: [],
        dataAnalysis: []
    }

    if (userType === 'default') {
        userPermissions.dashboard = ['r', 'w']
    }

    if (userType === 'analyst') {
        userPermissions.dashboard = ['r', 'w']
        userPermissions.dataAnalysis = ['r']
    }
    
    if (userType === 'admin') {
        userPermissions.dashboard = ['r', 'w']
        userPermissions.dataAnalysis = ['r']
        userPermissions.users = ['r', 'w']
    }

    return function(newUser) {
        const newUserObject = {
            user: newUser,
            permissions: userPermissions
        }
        registeredUsersDB.push(newUserObject)
        return newUserObject                         
    }
}

Analicemos que está pasando en esta función.

La lógica de asignar permisos sigue intacta, pero cambiaron los parámetros de entrada y el valor de retorno

Ahora como valor de retorno se regresa una función anónima. Esta función hace lo mismo que se hacía en la lógica anterior:

  • Empaqueta el usuario y los permisos en un nuevo objeto
  • Inserta el nuevo objeto en la base de datos (En nuestro caso un arreglo)
  • Regresa el objeto que se insertó

La diferencia es que esta función acepta el que era el 2° parámetro: newUser. Y porsupuesto ese parámetro ya no se acepta en nuestra función principal registerUser.

¿Cómo es el proceso para registrar un usuario de esta manera?

registerUser('default')({id: 1, userName: 'Solid'})
registerUser('analyst')({id: 2, userName: 'Liquid'})
registerUser('admin')({id: 3, userName: 'Solidus'})

Genial…¿supongo? Esta sintaxis se parece demasiado a la anterior. La única diferencia es que antes pasabamos el objeto como 2° parámetro después de una coma y ahora lo pasamos dentro de un paréntesis. ¿Eso de que ayuda?


Primero hay que entender que esta pasando dentro de registerUser.

Al regresar una función con registerUser, podemos llamar esa nueva función inmediatamente pasandole el parámetro newUser.

Una forma más fácil de leerlo sería:

const registerAnalystUser = registerUser('analyst')

registerAnalystUser({id: 1, userName: 'Solid'})

registerAnalystUser contiene la función anónima y por eso se le puede llamar con los paréntesis.

Este comportamiento nos permite crear una especie de “subfunciones” con diferentes tipos de usuario:

const registerAnalystUser = registerUser('analyst')
const registerDefaultUser = registerUser('default')
const registerAdminUser = registerUser('admin')

Cada subfunción contiene las referencias a sus respectivos userPermissions, por lo que solo necesitas pasar el newUser a la función correspondiente:

registerAnalystUser({id: 1, userName: 'Solid'})
registerDefaultUser({id: 2, userName: 'Liquid'})
registerAdminUser({id: 3, userName: 'Solidus'})

Currying es una de las bondades de la programación funcional. Algunas consideran que el código es mas legible de esta manera vs la forma tradicional.

Independientemente de tu opinión es útil que conozcas el concepto y la técnica, ya que muchas librerías se encuentran escritas de esta manera, y como bien sabes en JS utilizar librerías es el pan de cada día.