Control de flujo, expresiones de corto circuito y programación funcional en JavaScript

Publicado: 30/12/2021 · Tiempo de lectura: 5 minutos

Hace algunos días mientras revisaba un pull request me encontré con un fragmento de código que se veía algo así:

React.useEffect(() => {
someCondition && doSomething()
}, [someCondition, doSomething])

No es necesario que sepas que es lo que hace React.useEffect, solo quiero enfocarme en el cuerpo de la función que le pasamos como argumento.

Le sugerí a mi colega reemplazar la expresión de corto circuito para implementar dicha condición por un simple if. Cuando mi colega me preguntó por qué sería preferible utilizar un if, sinceramente no encontré una respuesta razonable. Solo se sentía mejor para mí.

Pero que algo se sienta mejor no es una razón aceptable, principalmente porque es muy subjetiva. Lo que se siente mejor para mí, no se va a sentir mejor necesariamente para el resto del equipo.

Así que hice lo que cualquier otra persona haría: me obsesioné (😅) e intenté buscar el motivo de por qué se siente mejor.

Aclaración: Este artículo solo describe mi razonamiento alrededor del tema. No estoy sugiriendo esto como buena práctica ni nada por el estilo.


Algo que he aprendido tras mirar muchas charlas de Rich Hickey, es siempre comenzar con una definición:

En ciencias de la computación, una expresión es una entidad sintáctica en un lenguaje de programación que podría ser evaluada para determinar su valor.

Algunas expresiones en JavaScript:

42 // 42
'foo' // 'foo'
false // false
const nums = [1, 2, 3] // ??
nums // [1, 2, 3]

En JavaScript, los valores literales se evalúan a sí mismos, y las variables se evalúan a cualquier valor que almacenen.

Veamos la cuarta línea en el fragmento de código anterior: en JavaScript, la asignación de una variable también es una expresión. ¿A qué crees que la expresión const nums = [1, 2, 3] se evalúa?

La asignación se evalúa como undefined.

En otros lenguajes de programación (como Python) la asignación de variables no son expresiones, sino que una declaración. Aquí está la definición de declaración:

En ciencias de la computación, una declaración es una unidad sintáctica de un lenguaje de programación imperativo que expresa alguna acción que se ejecutará.

Lo importante aquí es la palabra acción. Hablaremos de aquello en un momento.

Algunas declaraciones en JavaScript:

for (let n of nums) { /*...*/ }
while (true) { /*...*/ }
if (nums.length) { /*...*/ }

Ignorando el hecho de que la asignación es una expresión (una expresión inútil, si me preguntan) sería razonable pensar que las expresiones son a los valores como las declaraciones son a las acciones.

Evaluación de corto circuito

Más definiciones, yey:

Evaluación de corto circuito, evaluación mínima, o evaluación de McCarthy (por John McCarthy) se refiere a las semánticas de algunos operadores Boolean en algunos lenguajes de programación en donde el segundo argumento es ejecutado o evaluado solo si el primer argumento no es suficiente para determinar el valor de la expresión completa.

He aquí un ejemplo:

true || false // true

En el ejemplo anterior, la segunda parte de la expresión no es evaluada ya que el primer argumento es suficiente para determinar el valor de la expresión completa.

Es un poco extraño pensar así al utilizar literales, ya que los literales se evalúan a sí mismos. Lo escribiremos ligeramente distinto para que sea más fácil de razonar al respecto:

const aCondition = true
const anotherCondition = false
aCondition || anotherCondition // true

Dado que aCondition es true, no hay necesidad de evaluar la variable anotherCondition, independiente de cual sea su valor.

Revisemos con otro ejemplo:

const person = {
get name() {
console.log('Bayum!')
return 'Bodoque'
}
}
true || person.name // true

Al ejecutar este código vemos que "Bayum!" no es impreso en la consola, dado que el primer argumento de la expresión es true, lo que es genial.

¿Por qué esto es importante?

Efectos secundarios, programación funcional & Haskell

Haremos un pequeño desvío para continuar con –ya lo adivinaste–, otra definición:

Haskell es un lenguaje de propósito general, de tipado estático y puramente funcional con inferencia de tipos y evaluación perezosa.

Escribamos una pequeña función en Haskell que imprima "42" en la consola:

doSomething = putStrLn "42"

Utilizando ghci, el ambiente interactivo del compilador Glasgow Haskell Compiler (algo así como un REPL), podemos verificar el tipo de la función doSomething:

Prelude> doSomething = putStrLn "42"
Prelude> :t doSomething
doSomething :: IO ()

doSomething es una función que no toma ningún parámetro y cuyo tipo de retorno es IO (), o IO de unit (el par de paréntesis vacíos se llama unit y es similar a void en JavaScript). En Haskell todas las funciones con efectos secundarios tienen un tipo de retorno IO de algo. Las funciones puras no pueden invocar funciones con efectos. Si deseas tener un efecto, el tipo retornado siempre debe ser IO de algo.

Aunque no es obligación, podemos anotar explícitamente los tipos de nuestras funciones:

doSomething :: IO ()
doSomething = putStrLn "42"
-- He aquí otra función que toma dos Ints
-- y retorna otro Int
add :: Int -> Int -> Int
add a b = a + b

Ok, eso es todo lo que debíamos aprender sobre Haskell, de vuelta a nuestro itinerario habitual.

Expresiones de corto circuito y control de flujo

La invocación de una función siempre puede ser reemplazada por el valor retornado de la función, si este valor depende solo de sus parámetros. En otras palabras, la invocación de una función solo puede ser reemplazada por el valor retornado por la función solo si esta no tiene efectos secundarios.

Esta propiedad se conoce como transparencia referencial. Las funciones referencialmente transparentes también se conocen como funciones puras.

Cuando hacemos programación funcional, nuestro objetivo es maximizar la superficie de código que está implementada con funciones puras: son más fáciles de entender y más fáciles de testear. Así que para la mayor parte de las funciones en un programa, nos van a interesar los valores que retornan:

const whatIsThis = someCondition && doSomething()

Si no nos interesa el resultado de doSomething, probablemente no tiene valor guardar el valor de la expresión en whatIsThis. Aun así la expresión tiene un valor, aunque sea utilizado o no:

function doSomething() {
console.log("42")
}
someCondition && doSomething() // `false` cuando `someCondition` es `false`
// `undefined` cuando `someCondition` es `true`

Si nos nos interesa el valor de la expresión, entonces doSomething probablemente es una función con efectos. JavaScript no es Haskell así que no hay forma de decir si doSomething tiene efectos o no sin mirar su implementación. Incluso haciéndolo, no garantiza que sea algo sencillo de hacer.

Y creo que por esto es que prefiero utilizar un if en vez de una expresión de corto circuito para flujo de control en funciones con efectos: para mí es completamente inequívoco que no nos interesa el valor de retorno, y por ende entonces es un efecto secundario.

¿Y que hay de las funciones con efectos que sí retornan valores?

No tenemos un compilador como GHC para forzar pureza en nuestras funciones, pero igualmente podemos seguir una convención para invocar funciones con efectos solo dentro de otras funciones con efectos. En Haskell esto es posible por medio de monadas.

Si quieres una explicación más a fondo sobre este tema, te recomiendo el siguiente video:


Y eso ha sido todo por hoy. Si te ha gustado el contenido, no te olvides de darle una compartida en Twitter y seguirme por ahí.

Gracias totales y hasta la próxima.