Hoy volvemos a los básicos para sembrar las bases de como son los mecanismos por los cuáles las funciones funcionan. Hablaremos de variables, binding, entornos y closures.
En otros artículos ya hemos comentado lo que es un closure o como las variables enlazan valores, pero hoy le daremos otra vuelta.
Existe un tipo de lectura de la cual soy muy fan que se basa en la idea de que, en lugar de adelantarte todas las definiciones al principio, se va avanzando a lo largo de unos ejemplos y prácticas para que sea el lector el que descubra e interiorice los mecanismos que definen un concepto dado. Esta es la forma de escribir que sigue Reginald Braithwaite1 en sus ensayos y creo firmemente en su efectividad.
Además, me he basado mucho en el flujo del libro JavaScript Allongé2 de este mismo autor.
Qué es una función
Hay muchas definiciones sobre lo que es una función, no obstante, yo voy a recurrir a la más teórica para poder relacionarla con lo que veremos a continuación:
Una función es un valor que puede ser aplicado a otros valores o expresiones para producir nuevos valores.
Si te parecía que el definir que es una función era básico, es porque no sabías que, para entenderlas bien, tenemos que irnos más a lo básico, ya que para poder entender como un valor puede generar otros valores, tenemos que entender la diferencia entre valor y expresión.
Valores y expresiones
Todos los valores son expresiones. Si en un bar pedimos un americano3, lo que obtengo de la persona que me atiende es un americano. Es decir, "americano" es la expresión que utilizamos para obtener el valor "americano" y poder disfrutar de una buena taza de café de especialidad.
Por ejemplo, si en la consola de Javascript de Chrome le damos al intérprete el numero 42 (sentido de la vida4) lo que obtenemos a cambio es:
> 42
=> 42
Es decir, 42 es una expresión que le damos al intérprete y nos responde con la misma cosa, igual que con nuestro americano.
En cambio, si pedimos en el mismo bar pido "un vaso con agua caliente y un saquito de manzanilla", esto no es un valor. Es una expresión formada por dos valores:
Vaso con agua caliente
Saquito de manzanilla
Es el intérprete o la dueña del bar la que "interpreta" esa expresión y me devuelve el valor "manzanilla".
Así por ejemplo si definimos la expresión:
> "Hola" + "Mundo"
=> "Hola Mundo"
Lo que obtenemos es el valor "Hola Mundo".
Por ende, podemos afirmar que:
Un valor es una expresión.
Un conjunto de valores unidos por operadores, es una expresión.
Cuando una expresión se evalúa o "interpreta", obtenemos un valor.
Identidad de valores
Ahora que sabemos la diferencia entre valores y expresiones tenemos que hablar sobre bajo que circunstancias decimos que un valor es idéntico a otro. Para ello voy a recurrir a mi metáfora preferida: las cajas.
Así, trataremos a los tipos como si de cajas se tratasen. Por ejemplo, el string “hola mundo“ sería una caja de color azul con el contenido hola mundo.
De igual forma el número 1, es una caja digamos que de color roja y que contiene el número 1.
Vamos ahora a jugar con las identidades, para ello iremos viendo las 4 posibilidades a las que atiende el operador === de Javascript5 que usamos para comprobar la identidad entre valores:
Imagina que tenemos dos cajas iguales en forma y contenido, pero con diferente color: una roja y otra azul. Al diferir en color, decimos que no son idénticas:
> 1 === '1'
=> false
> true === 'true'
=> false
Mismo contenido, pero tipos distintos. Un número no es un string.
Ahora tenemos dos cajas iguales en forma y color, pero con diferente contenido: una tiene un "a" y otra "b":
> "a" === "b"
=> false
> true === false
=> false
Aquí estamos comparando valores en cajas idénticas respectivamente (string y boolean) pero con contenido diferente.
En tercer lugar, tenemos dos cajas iguales en forma, color y contenido. Solo que el contenido está representado de forma distinta: en un caso es una expresión y en otra es un valor. En este caso Javascript evalúa siempre todas las expresiones a un valor antes de compararlas. Por tanto:
> (1 + 1) === 2
=> true
> (1 + 1 === 2) === (4 !== 6)
=> true
Por tanto, si miramos las expresiones aparentemente son distintas entre las cajas, pero si las evaluamos antes de compararlas descubriremos que los valores son identicos:
> (2) === 2
> (true) === (true)
Mismo ejemplo que arriba, pero con las expresiones evaluadas
Finalmente podemos tener dos cajas exactamente iguales con exactamente el mismo contenido, pero con un número de serie distinto (algo así como el DNI de la caja) y ser por tanto cajas distintas:
> [1,2,3] === [1,2,3]
=> false
> [1+1, 1+1] === [2, 2]
=> false
Esto es lo que ocurre con los tipos referenciados6 o no primitivos. Así los ejemplos anteriores están construidos usando tipos primitivos (boolean, string, number, etc) y en este último hemos usado un no primitivo como el Array. Cada vez que creamos un array estamos creando un nuevo valor único.
Funciones como valores
Ahora bien, las funciones como hemos indicado al comienzo de este artículo, son valores. Es decir que si pasamos una función al intérprete de Javascript en Chrome7:
> () => 0
=> () => 0
Obtenemos la misma función de vuelta.
Nótese que el resultado puede variar según el intérprete que estemos usando. En un REPL o en NodeJs, el resultado puede ser [Function]. Esto es porque el hecho de que el intérprete nos devuelva algo, y mostrarlo por pantalla son cosas distintas. En algunas implementaciones se decidió que para el programador ver [Function] en términos de debug es más útil que ver el cuerpo de la función, sobre todo si estas son muy extensas.
¿Cómo se comportan las funciones cuando hablamos de identidad?
> (() => 0) === (() => 0)
=> false
Es decir, que al igual que ocurría con los Arrays, las funciones son tipos referenciados.
Ejecutando funciones
Como ya hemos visto, las funciones son valores especiales que nos permiten aplicarlos a otros valores o expresiones para obtener nuevos valores. Así, si ejecutamos la función definida anteriormente, obtendremos un valor:
> (() => 0)()
=> 0
Podemos decir que una función es un valor que devuelve el valor que colocamos a la derecha de la flecha
=>
.
¿Qué ocurre si en lugar de un valor, colocamos una expresión?
> (() => 1 + 1)()
=> 2
Una función no solo devuelve el valor colocado a la derecha de =>
, sino que evalúa todas las expresiones (y las transforma en valores) que coloquemos ahí.
Así, y como las funciones también son valores, una función puede devolver otra función:
> (() => () => 2))()()
=> 2
E incluso podría devolver el resultado de evalular una función:
> (() => (() => 2)())()
=> 2
Podemos decir por tanto que una función es un valor y podemos convertirla en una expresión que se evalúe a otro valor utilizando los paréntesis de aplicación
()
Devolviendo algo que no es ni un valor ni una expresión
No te he dicho que aparte de valores y expresiones, una función puede devolver una tercera cosa: un bloque8.
Los bloques se componen por cero o más sentencias (statements) y por defecto un bloque se evalúa siempre a undefined:
> (() => {})()
=> undefined
La ausencia de valor en Javascript está definida dentro del tipo undefined.
El tipo undefined es un valor y respeta las normas de identidad que vimos arriba:
> undefined === undefined
=> true
> (() => {})() === (() => {})()
=> true
> (() => {})() === undefined
Tenemos 3 formas por las cuales podemos obtener un undefined:
Evaluando una función que devuelva un valor (como hemos visto justo arriba).
Escribiendo nosotros mismos el valor undefined.
Utilizando el operador
void
9 que evalúa cualquier expresión a undefined:
> void 2
=> undefined
> void (1 + 1)
=> undefined
La forma más fiable de obtener un undefined es usando el operador void.
Para poder devolver un valor o el resultado de evaluar una expresión dentro de un bloque, usamos el operador return, el cual devuelve el resultado de evaluar la expresión de un statement. Y esta palabra reservada también es la encargada de terminar la ejecución de la función, por lo que todos los statements debajo de un return, no se ejecutarán.
Es importante recalcar que solo podemos usar return dentro de un bloque en una función. Por tanto, este código no sería válido: () => return 1; ya que estamos usando return fuera de un bloque.
Usando funciones
Antes hemos dicho que dado que las funciones son valores y las funciones devuelven valores, una función podría devolver otra función:
> () => () => 1
Por tanto, si aplicamos la primera función nos devuelve otra función – que es un valor – y si aplicamos esta última nos devuelve el valor 0
:
> (() => () => 1)()()
=> 1
En este caso, el resultado de la primera función es una función y el resultado de la segunda función es 1.
Esto también funciona con bloques:
> (() => () => { return "hola" })()()
=> "hola"
Argumentos
Los argumentos o parámetros de una función son los valores de entrada de la misma que utilizará para generar nuevos valores. Por ejemplo, una función toUpperCase que reciba un string como parámetro, tomará ese valor y lo devolverá convertido a mayúsculas.
Para escribir una función sin argumentos hacemos:
> () => {}
Y para escribir una función con argumentos:
> ((name) => {})("Sonia")
El argumento name crea una variable del mismo nombre que se enlazará con un valor en el momento de llamar a la función. En este caso, name se enlazará con “Sonia”.
Para definir más de un argumento, los separamos por comas:
> ((name, lastname) => {})("Sonia", "ramírez")
Los argumentos pasados a una función pueden ser usados en el cuerpo de la misma, ya sea un bloque:
> ((n) => { return n * 2; })(2)
=> 4
O fuera con la notación que vimos más arriba:
> ((n) => n * 2)(2)
=> 4
Lo mismo pasa con una función de dos o más argumentos:
> ((a, b) => a + b)(1, 2)
=> 3
Y de igual forma podemos llamar a una función pasándole como argumento una expresión:
> ((n) => n * 2)(2 + 2)
Llamada por valor
Dentro de todas las estrategias de evaluación que existen, la mayoría de lenguajes modernos (donde se incluye Javascript) utilizan la denominada "call by value"10. Esto implica que los valores o resultados de aplicar expresiones se enlazan con variables locales definidas exclusivamente para la función. Por tanto, si intentamos reasignar un la variable de un argumento a otra cosa, solo estaría cambiando el enlace de esa variable a otra cosa, sin alterar por supuesto el valor original.
Creando entornos con funciones
Observemos esta función:
> (x) => x
La parte de más a la izquierda de (x)...
está definiendo un nuevo argumento llamado x
. En cambio, la parte de más a la derecha ...=> x
no es un parámetro, si no una expresión que hace referencia a una variable, una que ha sido definida en el entorno de esa función.
Y es que cada vez que aplicamos una función, se crea un entorno para esa función el cual establece una relación entre el nombre de todos los argumentos de esa función y sus valores, obtenidos de la aplicación. Por ejemplo, si aplicamos la función anterior al valor 2:
> ((x) => x)(2)
=> 2
La función devuelve 2, que es el valor al que la x se ha enlazado cuando se creó el entorno.
El flujo simplificado que ha ocurrido para obtener el resultado de 2, es el siguiente:
Javascript parsea la función.
Entonces, evalúa la función aplicándola al argumento 2.
Se crea un nuevo entorno.
El valor 2 se enlaza al nombre x en ese entorno.
La expresión x (de la parte derecha de la función) es evaluada en el entorno.
Cuando una variable se evalúa en un entorno, esta toma el valor que esté definido en ese entorno. En este caso para x es 2.
Finalmente, se devuelve el resultado.
A modo de ejemplificar como quedaría nuestro entorno, podemos suponer que la representación del mismo se hace usando diccionarios u objetos en Javascript. Por tanto, el entorno creado para la función anterior sería algo así:
{
x: 2
}
Closures
Los closures – pronunciado ˈklōZHər – son una de esas cosas que es difícil de intuir su funcionamiento sin conocer lo que hemos aprendido anteriormente. Y es que es común caer en la idea de que como habitualmente una función definida dentro de otra función decimos que es un closure, tendemos a generalizar ese concepto y llegar a afirmar que literalmente un closure es cualquier función definida en el cuerpo de otra. Y no es correcto.
Analicemos este ejemplo:
> ((x) => (y) => x + y)(1)(2)
=> 3
Si intentamos evaluar la función más interior, (y) => x + y
nos daremos cuenta de que nos falta información. Concretamente el valor de la variable x puesto que al no estar definida en los parámetros de la función (o, mejor dicho, en su entorno) tendremos que acceder a ella de alguna otra forma.
La variable que no está definida en los argumentos de una función dada, se conoce como 'variable libre'.
Arriba hemos dicho que todas las funciones tienen un entorno asignado que es creado en el momento en que la función es aplicada, de tal forma que de manera individual (y siguiendo la notación por diccionarios que hemos definido antes) podemos sacar el supuesto entorno de cada una de las funciones:
(x) => (y) => x + y // { x: 1 } entorno de la primera función
(y) => x + y // { y: 2 } entorno de la segunda función
Sin embargo, esto no es del todo correcto, ya que un entorno de una función definida en el cuerpo de otra, guardará una referencia al entorno donde se creó. Ósea, que el entorno de una función, no solo tiene acceso a las variables definidas en la misma, si no que tiene acceso al entorno “padre”. Así:
(x) => (y) => x + y // { x: 1, ..: 'global' }
(y) => x + y // { y: 2, ..: { x: 1, ..: 'global'}
Aquí estoy referenciando al padre con el identificador ‘..’. Símbolo usado frecuentemente en sistemas de ficheros.
Date cuenta que el entorno de la función de fuera guarda una referencia al entorno global. Esto es porque el entorno donde se ejecuta la función de fuera (en nuestro caso) es el global.
Y ahora, sí que tenemos toda la información para alcanzar nuestras variables (libres o no) y sus correspondientes valores. Ya que, si Javascript no encuentra los valores para las variables en su propio entorno, irá a buscarlos al padre y sino al abuelo, etc.
((x) => // { x: 1, ..: 'global' }
(y) => // { y: 2, ..: { x: 1, ..: 'global' } }
(z) => x + y + z)(1)(2)(3)
// { z: 3, ..: { y: 2, ..: { x: 1, ..: global }} }
Por tanto, ahora podemos afirmar que:
Funciones que no contienen variables libres se llaman funciones puras.
Funciones que contienen una o más variables libres se llaman closures.
Y así:
let a = 1;
function test() { // función closure con una variable libre
return a + 1;
}
function outer() { // función pura sin variables libres
function inner(name) { // función pura sin variables libres
console.log(name);
}
}
function test () { // función closure con una variable libre
let b = a;
return function inner() { // función closure con una variable libre
return b;
}
}
No ya que todas las funciones definidas dentro de funciones no sean closures. Si no que funciones no definidas dentro de otras funciones pueden ser closures.
Conclusión
Hemos dado un repaso a los mecanismos por el cual las funciones funcionan.
Espero que hayas entendido la diferencia entre valores y expresiones y como se evalúan, por qué tratamos las funciones como valores y que es realmente un closure.
En futuras entregas profundizaremos en como las funciones enlazan y definen variables, cómo estas delimitan su scope y cómo podemos hacer shadow sobre otras variables.
Nada más por hoy :).
https://raganwald.com/
https://leanpub.com/javascriptallongesix/read
https://es.wikipedia.org/wiki/Caf%C3%A9_americano
https://es.wikipedia.org/wiki/El_sentido_de_la_vida,_el_universo_y_todo_lo_dem%C3%A1s
https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Strict_equality
https://en.wikipedia.org/wiki/Value_type_and_reference_type
https://developer.chrome.com/docs/devtools/console/
https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/block
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void
https://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_value