¿Qué es una función de devolución de llamada?
¿Qué es una función de devolución de llamada?
Los desarrolladores a menudo se confunden sobre lo que es una devolución de llamada debido al nombre de la maldita cosa.
Una función de devolución de llamada es una función que es:
- accesible mediante otra función, y
- se invoca después de la primera función si esa primera función se completa
Una buena forma de imaginar cómo funciona una función de devolución de llamada es que es una función que se " llama al final " de la función a la que se pasa.
Quizás un mejor nombre sería una función de "llamada después" .
Esta construcción es muy útil para comportamientos asincrónicos en los que queremos que se lleve a cabo una actividad cada vez que se completa un evento anterior.
Pseudocódigo:
// A function which accepts another function as an argument
// (and will automatically invoke that function when it completes - note that there is no explicit call to callbackFunction)
funct printANumber(int number, funct callbackFunction) {
printout("The number you provided is: " + number);
}
// a function which we will use in a driver function as a callback function
funct printFinishMessage() {
printout("I have finished printing numbers.");
}
// Driver method
funct event() {
printANumber(6, printFinishMessage);
}
Resultado si llamaste al evento():
The number you provided is: 6
I have finished printing numbers.
El orden de salida aquí es importante. Dado que las funciones de devolución de llamada se llaman después, "Terminé de imprimir los números" se imprime al final, no al principio.
Las devoluciones de llamada reciben este nombre debido a su uso con lenguajes de puntero. Si no utiliza uno de esos, no se preocupe por el nombre "devolución de llamada". Simplemente comprenda que es solo un nombre para describir un método que se proporciona como argumento a otro método, de modo que cuando se llama al método principal (cualquiera que sea la condición, como un clic en un botón, un tic del temporizador, etc.) y se completa el cuerpo de su método, Luego se invoca la función de devolución de llamada.
Algunos lenguajes admiten construcciones donde se admiten múltiples argumentos de función de devolución de llamada y se llaman en función de cómo se completa la función principal (es decir, se llama a una devolución de llamada en caso de que la función principal se complete correctamente, se llama a otra en caso de que la función principal arroje un error específico, etc.).
Definición opaca
Una función de devolución de llamada es una función que usted proporciona a otro fragmento de código, lo que permite que ese código lo llame.
ejemplo artificial
Por qué querrías hacer esto? Digamos que hay un servicio que necesitas invocar. Si el servicio regresa inmediatamente, simplemente:
- Llámalo
- Espera el resultado
- Continúe una vez que llegue el resultado.
Por ejemplo, supongamos que el servicio fuera la factorial
función. Cuando desee el valor de 5!
, invocará factorial(5)
y se producirán los siguientes pasos:
Su ubicación de ejecución actual se guarda (en la pila, pero eso no es importante)
La ejecución se entrega a
factorial
Cuando
factorial
se completa, coloca el resultado en algún lugar al que pueda acceder.La ejecución vuelve a donde estaba [1]
Ahora supongamos factorial
que tomó mucho tiempo, porque le estás dando números enormes y necesita ejecutarse en algún clúster de supercomputación en algún lugar. Supongamos que espera que le lleve 5 minutos obtener el resultado. Tú podrías:
Mantenga su diseño y ejecute su programa por la noche cuando esté dormido, para no estar mirando la pantalla la mitad del tiempo.
Diseña tu programa para hacer otras cosas mientras
factorial
hace lo suyo
Si elige la segunda opción, las devoluciones de llamada podrían funcionar para usted.
Diseño de extremo a extremo
Para explotar un patrón de devolución de llamada, lo que desea es poder llamar factorial
de la siguiente manera:
factorial(really_big_number, what_to_do_with_the_result)
El segundo parámetro, what_to_do_with_the_result
, es una función que usted envía a factorial
, con la esperanza de que factorial
lo invoque según su resultado antes de regresar.
Sí, esto significa que factorial
debe haberse escrito para admitir devoluciones de llamadas.
Ahora suponga que desea poder pasar un parámetro a su devolución de llamada. Ahora no puedes, porque no vas a llamarlo factorial
. Por lo tanto, factorial
debe escribirse para permitirle pasar sus parámetros y simplemente los entregará a su devolución de llamada cuando lo invoque. Podría verse así:
factorial (number, callback, params)
{
result = number! // i can make up operators in my pseudocode
callback (result, params)
}
Ahora que factorial
permite este patrón, su devolución de llamada podría verse así:
logIt (number, logger)
{
logger.log(number)
}
y tu llamada factorial
sería
factorial(42, logIt, logger)
¿ Qué pasa si quieres devolver algo de logIt
? Bueno, no puedes, porque factorial
no le está prestando atención.
Bueno, ¿por qué no puedes factorial
simplemente devolver lo que devuelve tu devolución de llamada?
Haciéndolo sin bloqueo
Dado que la ejecución debe entregarse a la devolución de llamada cuando factorial
finalice, realmente no debería devolver nada a la persona que llama. E idealmente, de alguna manera iniciaría su trabajo en otro hilo/proceso/máquina y regresaría inmediatamente para que usted pueda continuar, tal vez algo como esto:
factorial(param_1, param_2, ...)
{
new factorial_worker_task(param_1, param_2, ...);
return;
}
Ahora es una "llamada asincrónica", lo que significa que cuando la llamas, regresa inmediatamente pero aún no ha hecho su trabajo. Por lo tanto, necesita mecanismos para verificarlo y obtener su resultado cuando esté terminado, y su programa se ha vuelto más complejo en el proceso.
Y, por cierto, al usar este patrón, factorial_worker_task
puede iniciar su devolución de llamada de forma asincrónica y regresar inmediatamente.
Entonces, ¿Qué haces?
La respuesta es permanecer dentro del patrón de devolución de llamada. Cuando quieras escribir
a = f()
g(a)
y f
debe llamarse de forma asincrónica, en su lugar escribirá
f(g)
donde g
se pasa como devolución de llamada.
Esto cambia fundamentalmente la topología de flujo de su programa y requiere algo de tiempo para acostumbrarse.
Su lenguaje de programación podría ayudarlo mucho al brindarle una forma de crear funciones sobre la marcha. En el código inmediatamente anterior, la función g
podría ser tan pequeña como print (2*a+1)
. Si su lenguaje requiere que defina esto como una función separada, con un nombre y una firma completamente innecesarios, entonces su vida se volverá desagradable si usa mucho este patrón.
Si, por otro lado, tu lenguaje te permite crear lambdas, entonces estás en mucho mejor forma. Luego terminarás escribiendo algo como
f( func(a) { print(2*a+1); })
que es mucho mejor.
Cómo pasar la devolución de llamada
¿ Cómo le pasarías la función de devolución de llamada factorial
? Bueno, puedes hacerlo de varias maneras.
Si la función llamada se ejecuta en el mismo proceso, puede pasar un puntero de función
O tal vez quieras mantener un diccionario de
fn name --> fn ptr
en tu programa, en cuyo caso podrías pasar el nombreTal vez su idioma le permita definir la función en el lugar, ¡posiblemente como una lambda! Internamente se trata de crear algún tipo de objeto y pasar un puntero, pero no tienes que preocuparte por eso.
Quizás la función a la que está llamando se esté ejecutando en una máquina completamente separada y la esté llamando mediante un protocolo de red como HTTP. Puede exponer su devolución de llamada como una función invocable por HTTP y pasar su URL.
Entiendes la idea.
El reciente aumento de las devoluciones de llamadas
En esta era de la web en la que hemos entrado, los servicios que invocamos suelen realizarse a través de la red. A menudo no tenemos ningún control sobre esos servicios, es decir, no los escribimos, no los mantenemos, no podemos asegurarnos de que estén funcionando o de su rendimiento.
Pero no podemos esperar que nuestros programas se bloqueen mientras esperamos que estos servicios respondan. Siendo conscientes de esto, los proveedores de servicios suelen diseñar API utilizando el patrón de devolución de llamada.
JavaScript admite muy bien las devoluciones de llamadas, por ejemplo, con lambdas y cierres. Y hay mucha actividad en el mundo de JavaScript, tanto en el navegador como en el servidor. Incluso se están desarrollando plataformas JavaScript para dispositivos móviles.
A medida que avancemos, cada vez más de nosotros escribiremos código asincrónico, por lo que esta comprensión será esencial.
La página de devolución de llamada en Wikipedia lo explica muy bien:
En programación de computadoras, una devolución de llamada es una referencia a un código ejecutable, o un fragmento de código ejecutable, que se pasa como argumento a otro código. Esto permite que una capa de software de nivel inferior llame a una subrutina (o función) definida en una capa de nivel superior.
Explicación simple por analogía
Todos los días me pongo a trabajar. El jefe me dice:
Ah, y cuando hayas terminado con eso, tengo una tarea adicional para ti:
Excelente. Me entrega una nota con una tarea: esta tarea es una función de devolución de llamada. Podría ser cualquier cosa:
ben.doWork( and_when_finished_wash_my_car)
Mañana podría ser:
ben.doWork( and_tell_me_how_great_i_am)
¡El punto clave es que la devolución de la llamada debe realizarse DESPUÉS de que termine el trabajo! Haría bien en leer el código contenido en otras respuestas anteriores, que no necesito repetir.
Una respuesta sencilla sería que es una función que no la llama usted, sino el usuario o el navegador después de que haya ocurrido un determinado evento o después de que se haya procesado algún código.