Nonsense

Complicando las cosas sencillas

El problema de David, básicamente concierne a este trozo de código:

var source = new Buffer(10);
// El objeto source no contiene nada
console.log(source.toString('utf-8'));
 
fs.readFile('home.html', function(error, source){
  if(error) {
    console.log(error);
  } else {
    // El objeto source contiene los datos del fichero
    console.log(source.toString('utf-8'));
  }
});

Inicialmente, hacía esta pregunta:

¿Por qué no funciona el paso por referencia en el callback del método readFile? ¿Por qué dentro de la función la variable source tiene un valor y fuera vuelve a tener otro? ¿Me estoy equivocando al referenciar a la variable global como parámetro?

Pero luego, en Twitter, añade:

…probé a declarar la función en otro lado y pasarla como parámetro con variables ya declaradas. tampoco conseguí lo que pretendía que es responder la siguiente pregunta ¿cómo pasar una variable como parámetro a un callback?

El problema de David, o esa es la impresión que tengo yo, es el de mezclar demasiadas cosas y no probar lo más sencillo de todo.

Lo que entiendo que quiere hacer

Lo que entiendo que quiere hacer David es leer un fichero. Para eso, llama a fs.readFile y quiere obtener sus resultados en el Buffer que ha creado un poco antes, source. Pero a fs.readFile lo que se le pasa como parámetro es una función de callback. Entonces intenta hacer referencia a source desde esa función diciendo, de algún modo, que el resultado lo quiere en source. Ese “algún modo” que intenta es llamar source al parámetro del callback.

La solución es infinitamente más sencilla, pero a la vez no hay solución, porque las cosas no son como las está pensando David. Primero explicaré por qué no se puede hacer lo que intenta hacer y luego explicaré cómo se hace lo que quiere hacer.

Ni aquí, ni en ningún sitio

Lo que intentas hacer, David, no se puede hacer. Ni en JavaScript, ni en Java, ni en C, ni en nada 1). No es un tema de “cosas raras de JavaScript” ni nada similar, es que estás enrevesando las cosas intentando algo que no tiene sentido. Muy probablemente porque hay otra cosa interfiriendo que es a lo que no estás acostumbrado.

Básicamente, el problema es el siguiente. Tenemos 3 elementos:

- la variable global source - una función – que llamaré callback - una llamada a fs.readFile

En código:

var source = new Buffer(10); // 1
 
function callback( ... ) { // 2
    // ...
}
 
fs.readFile('home.html', callback); // 3

He dejado momentáneamente los parámetros de callback sin definir intencionadamente. Lógicamente, el código de calback no funcionaría sin esos parámetros así que también lo omito por el momento.

Bien, el motivo por el que no funciona lo que intentas hacer es simple: Tú no estás llamando a callback, la estás definiendo. Es decir, es la diferencia entre parámetros formales y reales. Cuando escribimos:

function callback(error, source) {
    // ...
}

Sabemos que error y source no son variables, no son referencias, son los parámetros formales de la función. Incluso aunque fuera C y definiéramos que source debe ser un puntero…

void callback(int error, something *source) {
    // ...
}

…ese parámetro formal source ahí no es ninguna referencia. source será una referencia cuando llamemos a callback y le pasemos como parámetro una referencia. No hay una manera, ni en JavaScript ni en C, de definir un parámetro formal diciendo que “en realidad esto apunta a esa otra variable”. 2)

Ahora, seguramente la confusión viene en parte de la ausencia de tipado y en parte de el uso de callbacks. Si intentamos analizar un poco el tema de los tipos, podríamos decir que la firma de fs.readFile es:

String -> ((Integer, String) -> ()) -> ()

Escrito con anotaciones de tipo como las de TypeScript, podría ser algo como…

// Este es el tipo de la función de callback que admite readFile:
interface funcionesDeCallbackParaReadFile {
  (error: number, source: string): void;
}
// Este sería más o menos el de readFile:
function readFile(filename:string, callback:funcionesDeCallbackParaReadFile) :void {
    // ...
}

Así queda más claro que quien llama a callback no eres tú, sino que es readFile quien lo llama. Y cuando lo llame, lo llamará pasándole “lo que le dé la gana pasarle”. readFile ya creará su buffer o un String o lo que quiera crear.

Sería distinto si la firma de readFile fuera algo parecido a esto otro:

function readFile(filename:string, source:Buffer) :void {
    // ...
}

Si las cosas fueran así, entonces estaría claro que somos nosotros quienes le pasamos algo (un Buffer) a fs.readFile. Y entonces sí, tenemos que crear nosotros el Buffer y pasárselo. En C, esta forma de hacer las cosas era bastante habitual, porque así era el código cliente el que se ocupaba de manejar la memoria que le correspondía y sabía cuándo tenía que reservar el buffer o liberarlo.

Las cosas no son así en JavaScript, sino que son como decía antes. Usar un callback crea una cierta confusión al pensar quién está llamando a qué. Pero esto es algo a lo que es fundamental prestar atención siempre que usemos asincronía.

La solucion

¿Cómo se hace esto en JavaScript? En realidad, como he dicho, no se hace. Pero la forma en que se hacen este tipo de cosas es mucho más sencilla de lo que parece. Veamos de nuevo el código original y la segunda pregunta, la de Twitter (aunque la voy a cambiar un poco):

var source = new Buffer(10);
 
fs.readFile('home.html', function(error, source){
    // ¿Cómo puedo hacer uso aquí del source de ahí fuera??
});

Con el código escrito así, no puedes. Y ahora el problema está simplemente en que el nombre del parámetro formal oculta el nombre de la variable global. Es decir, es sólo un problema de nombres. Si cambiamos uno de los dos, todo se arregla:

var source = new Buffer(10);
 
fs.readFile('home.html', function(error, htmlContent){
    console.log(htmlContent); // fs.readFile te pasará el contenido de home.html en esta variable
    // ¿Cómo puedo hacer uso aquí del source de ahí fuera??
    // Simplemente hazlo:
    console.log(source); // El buffer vacío!
    source = htmlContent; // Le metemos lo que nos ha dado fs.readFile
});

No hay más misterio. Desde una función, puedes acceder a variables de un contexto superior sin más problema… Siempre que no haya otro nombre (p.ej. de un parámetro, o de una variable local) que lo oculte, claro.

Ahora bien...

Hay algunos detalles un poco en el aire. Nos metemos un poco en temas más avanzados, de organización, de buenas prácticas y eso, pero bueno, te lo cuento de todos modos. Intuyo que la idea es cargar el fichero home.html al arrancar el servidor probablemente con la idea de sólo leerlo una vez, optimizar y todo eso.

Es una buena idea, por lo menos en algunas situaciones. Sin embargo, también presenta algunas desventajas. La más evidente es que si luego modificas home.html, el servidor no va a volver a cargarlo a no ser que lo vuelvas a arrancar.

Por otra parte, aunque en este caso sea muy improbable que no ocurra así, estás confiando en que fs.readFile termina de leer tu fichero antes de que express levante el servidor. Como digo es muy improbable que falle eso, pero como práctica general no es bueno apoyarse en improbabilidades. Sobre todo porque cuando lo haces con un sólo fichero no pasa nada, pero si luego tratas de implementar eso mismo para más ficheros, la improbabilidad disminuye.

En realidad, lo normal sería cargar el fichero en cada petición. Luego si ves que realmente eso te da problemas de rendimiento, entonces plantéate implementar una caché de lectura de ficheros. No suelo usar express últimamente, pero es muy probable que ya tengas algo hecho o que más probablemente tengas algún módulo de express que use alguna otra librería para hacerlo (ya que el problema es general, no sólo del entorno de express).

Así sin buscar mucho, he visto que hay un express-init por lo menos para añadir robustez a la inicialización y no simplemente “confiar en que fs.readFile termina antes”. Seguramente habrá algún middleware para el tema de cachear ficheros locales… pero ojo, porque hay que hacer equilibrio entre cachear y consumir memoria… Y ya sabes lo mucho que le gusta a Node consumir memoria ;)

Para terminar: Sé que esto es un tocho infumable. Espero que por lo menos sea más o menos inteligible. Sea como sea, escribir esto no anula de ningún modo la oferta de explicártelo en persona delante de una cerveza (o, mejor, de un helado) :)

1)
Si alguien encuentra un lenguaje en el que se puede, no hace falta que me lo digáis :p
2)
En ES2015, con la introducción de valores por defecto, habrá algo muy parecido a esto, pero incluso así, no es lo mismo