programacion - Punteros de función, cierres y lambda
lambda function aws (12)
Ahora mismo estoy aprendiendo sobre los indicadores de función y, mientras leía el capítulo de K & R sobre el tema, lo primero que me llamó la atención fue: "Oye, esto es como un cierre". Sabía que esta suposición es fundamentalmente errónea de alguna manera y después de una búsqueda en línea no encontré realmente ningún análisis de esta comparación.
Entonces, ¿por qué los punteros de función estilo C son fundamentalmente diferentes de los cierres o lambdas? Por lo que puedo decir, tiene que ver con el hecho de que el puntero a la función aún apunta a una función definida (nombrada) en oposición a la práctica de definir la función anónimamente.
¿Por qué pasar una función a una función vista como más poderosa en el segundo caso, donde no tiene nombre, que la primera donde solo se pasa una función normal y cotidiana?
Por favor dígame cómo y por qué me equivoco al comparar los dos tan de cerca.
Gracias.
Cierre = lógica + entorno.
Por ejemplo, considere este método C # 3:
public Person FindPerson(IEnumerable<Person> people, string name)
{
return people.Where(person => person.Name == name);
}
La expresión lambda no solo encapsula la lógica ("comparar el nombre") sino también el entorno, incluido el parámetro (es decir, la variable local) "nombre".
Para más información sobre esto, eche un vistazo a mi artículo sobre cierres que lo lleva a través de C # 1, 2 y 3, que muestra cómo los cierres hacen las cosas más fáciles.
Como alguien que ha escrito compiladores para idiomas con y sin cierres ''reales'', estoy respetuosamente en desacuerdo con algunas de las respuestas anteriores. Un cierre de Lisp, Scheme, ML o Haskell no crea una nueva función de forma dinámica . En su lugar, reutiliza una función existente, pero lo hace con nuevas variables gratuitas . La recopilación de variables gratuitas a menudo se denomina entorno , al menos por teóricos del lenguaje de programación.
Un cierre es simplemente un agregado que contiene una función y un entorno. En el compilador de ML estándar de Nueva Jersey, representamos uno como registro; un campo contenía un puntero al código, y los otros campos contenían los valores de las variables libres. El compilador creó un nuevo cierre (no función) dinámicamente asignando un nuevo registro que contiene un puntero al mismo código, pero con diferentes valores para las variables libres.
Puedes simular todo esto en C, pero es un dolor en el culo. Dos técnicas son populares:
Pase un puntero a la función (el código) y un puntero separado a las variables libres, de modo que el cierre se divida entre dos variables C.
Pase un puntero a una estructura, donde la estructura contiene los valores de las variables libres y también un puntero al código.
La técnica n. ° 1 es ideal cuando intentas simular algún tipo de polimorfismo en C y no quieres revelar el tipo de entorno: utilizas un puntero void * para representar el entorno. Por ejemplo, mire las Interfaces e Implementaciones C de Dave Hanson. La técnica n.º 2, que se parece más a lo que sucede en los compiladores de código nativo para los lenguajes funcionales, también se parece a otra técnica familiar ... Objetos de C ++ con funciones de miembros virtuales. Las implementaciones son casi idénticas.
Esta observación condujo a una broma de Henry Baker:
La gente en el mundo de Algol / Fortran se quejó durante años de que no entendían qué posible uso podrían tener los cierres de funciones en la programación eficiente del futuro. Luego ocurrió la revolución `programación orientada a objetos '', y ahora todos programan usando cierres de funciones, excepto que aún se niegan a llamarlos así.
El cierre captura las variables libres en un entorno . El entorno seguirá existiendo, aunque el código circundante ya no esté activo.
Un ejemplo en Common Lisp, donde MAKE-ADDER
devuelve un nuevo cierre.
CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta)))
MAKE-ADDER
CL-USER 54 > (compile *)
MAKE-ADDER
NIL
NIL
Usando la función anterior:
CL-USER 55 > (let ((adder1 (make-adder 0 10))
(adder2 (make-adder 17 20)))
(print (funcall adder1))
(print (funcall adder1))
(print (funcall adder1))
(print (funcall adder1))
(print (funcall adder2))
(print (funcall adder2))
(print (funcall adder2))
(print (funcall adder1))
(print (funcall adder1))
(describe adder1)
(describe adder2)
(values))
10
20
30
40
37
57
77
50
60
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment #(77 20)
Tenga en cuenta que la función DESCRIBE
muestra que los objetos de función para ambos cierres son los mismos, pero el entorno es diferente.
Common Lisp hace que tanto los cierres como los objetos de función puros (aquellos sin un entorno) sean funciones y uno puede llamar a ambos de la misma manera, aquí usando FUNCALL
.
En C no puede definir la función en línea, por lo que no puede crear realmente un cierre. Todo lo que estás haciendo es pasar una referencia a algún método predefinido. En los lenguajes que admiten métodos anónimos / cierres, la definición de los métodos es mucho más flexible.
En los términos más simples, los punteros a las funciones no tienen ningún alcance asociado (a menos que cuente el alcance global), mientras que los cierres incluyen el alcance del método que los define. Con lambdas, puedes escribir un método que escribe un método. Los cierres le permiten vincular "algunos argumentos a una función y obtener una función de menor gravedad como resultado". (tomado del comentario de Thomas). No puedes hacer eso en C.
EDITAR: Agregar un ejemplo (voy a usar la sintaxis Actionscript-ish porque eso es lo que tengo en mente en este momento):
Supongamos que tiene algún método que toma otro método como argumento, pero no proporciona una forma de pasar ningún parámetro a ese método cuando se lo llama. Como, por ejemplo, algún método que causa un retraso antes de ejecutar el método que pasó (ejemplo estúpido, pero quiero que sea sencillo).
function runLater(f:Function):Void {
sleep(100);
f();
}
Ahora di que quieres que el usuario runLater () demore el procesamiento de un objeto:
function objectProcessor(o:Object):Void {
/* Do something cool with the object! */
}
function process(o:Object):Void {
runLater(function() { objectProcessor(o); });
}
La función que está pasando a procesar () ya no es una función definida estáticamente. Se genera dinámicamente y puede incluir referencias a variables que estaban dentro del alcance cuando se definió el método. Por lo tanto, puede acceder a ''o'' y ''objectProcessor'', aunque no estén en el alcance global.
Espero que tenga sentido.
En C, los punteros a las funciones se pueden pasar como argumentos a las funciones y se devuelven como valores de las funciones, pero las funciones solo existen en el nivel superior: no se pueden anidar las definiciones de funciones entre sí. Piense en lo que le llevaría a C soportar las funciones anidadas que pueden acceder a las variables de la función externa, al mismo tiempo que puede enviar punteros de función hacia arriba y hacia abajo en la pila de llamadas. (Para seguir esta explicación, debe conocer los conceptos básicos de cómo se implementan las llamadas de función en C y en la mayoría de los lenguajes similares: examine la entrada de la pila de llamadas en Wikipedia).
¿Qué tipo de objeto es un puntero a una función anidada? No puede ser simplemente la dirección del código, porque si lo llamas, ¿cómo accede a las variables de la función externa? (Recuerde que debido a la recursión, puede haber varias llamadas diferentes de la función externa activa al mismo tiempo.) Esto se conoce como el problema de funarg , y hay dos subproblemas: el problema de funargs descendente y el problema de funargs ascendente.
El problema de los funargs descendentes, es decir, enviar un puntero de función "abajo de la pila" como argumento a una función que usted llama, en realidad no es incompatible con C, y GCC supports funciones anidadas como funargs descendentes. En GCC, cuando crea un puntero a una función anidada, realmente obtiene un puntero a un trampoline , una pieza de código construida dinámicamente que configura el puntero de enlace estático y luego llama a la función real, que utiliza el puntero de enlace estático para acceder las variables de la función externa.
El problema de los simulacros ascendentes es más difícil. GCC no le impide dejar que exista un puntero de trampolín después de que la función externa ya no esté activa (no tiene registro en la pila de llamadas) y, a continuación, el puntero del enlace estático podría apuntar a la basura. Los registros de activación ya no se pueden asignar en una pila. La solución habitual es asignarlos en el montón, y dejar que un objeto de función que representa una función anidada simplemente apunte al registro de activación de la función externa. Tal objeto se llama closure . Entonces, el lenguaje generalmente tendrá que admitir la recolección de basura para que los registros se puedan liberar una vez que ya no haya punteros apuntando a ellos.
Las lambdas ( funciones anónimas ) son realmente un problema aparte, pero generalmente un lenguaje que le permite definir funciones anónimas sobre la marcha también le permitirá devolverlas como valores de función, por lo que terminan siendo cierres.
En C, un puntero a función es un puntero que invocará una función cuando la desreferencia, un cierre es un valor que contiene la lógica de una función y el entorno (variables y valores a los que están vinculados) y una lambda usualmente se refiere a un valor que es en realidad una función sin nombre. En C, una función no es un valor de primera clase, por lo que no se puede pasar, por lo que debe pasarle un puntero; sin embargo, en los lenguajes funcionales (como Scheme) puede pasar funciones de la misma manera que pasa cualquier otro valor
En GCC, es posible simular funciones lambda utilizando la siguiente macro:
#define lambda(l_ret_type, l_arguments, l_body) /
({ /
l_ret_type l_anonymous_functions_name l_arguments /
l_body /
&l_anonymous_functions_name; /
})
Ejemplo de source :
qsort (array, sizeof (array) / sizeof (array[0]), sizeof (array[0]),
lambda (int, (const void *a, const void *b),
{
dump ();
printf ("Comparison %d: %d and %d/n",
++ comparison, *(const int *) a, *(const int *) b);
return *(const int *) a - *(const int *) b;
}));
El uso de esta técnica, por supuesto, elimina la posibilidad de que su aplicación funcione con otros compiladores y aparentemente es un comportamiento "indefinido" para el YMMV.
La mayoría de las respuestas indican que los cierres requieren punteros de función, posiblemente a funciones anónimas, pero como Mark escribió, los cierres pueden existir con funciones nombradas. Aquí hay un ejemplo en Perl:
{
my $count;
sub increment { return $count++ }
}
El cierre es el entorno que define la variable $count
. Solo está disponible para la subrutina de increment
y persiste entre llamadas.
La principal diferencia surge de la falta de alcance léxico en C.
Un puntero a la función es solo eso, un puntero a un bloque de código. Cualquier variable que no sea de pila a la que haga referencia es global, estática o similar.
Un cierre, OTOH, tiene su propio estado en forma de ''variables externas'' o ''valores ascendentes''. pueden ser tan privados o compartidos como desee, utilizando el alcance léxico. Puede crear muchos cierres con el mismo código de función, pero instancias de variables diferentes.
Algunos cierres pueden compartir algunas variables, y también puede ser la interfaz de un objeto (en el sentido OOP). para hacer eso en C tienes que asociar una estructura con una tabla de indicadores de función (eso es lo que hace C ++, con una clase vtable).
en resumen, un cierre es un puntero a la función MÁS un estado. es una construcción de alto nivel
Los cierres implican que alguna variable desde el punto de definición de la función está unida a la lógica de la función, como la posibilidad de declarar un mini objeto sobre la marcha.
Un problema importante con C y los cierres es que las variables asignadas en la pila se destruirán al salir del alcance actual, independientemente de si un cierre estaba apuntando a ellas. Esto llevaría al tipo de errores que las personas obtienen cuando devuelven indicios descuidados a las variables locales. Los cierres básicamente implican que todas las variables relevantes son artículos con recuento o recolectados en la basura en un montón.
No me siento cómodo equiparando lambda con cierre porque no estoy seguro de que las lambdas en todos los idiomas sean cierres, a veces creo que las lambdas han sido funciones anónimas definidas localmente sin el enlace de variables (Python pre 2.1?).
Una lambda (o closure ) encapsula tanto el puntero de función como las variables. Es por eso que, en C #, puedes hacer:
int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
return i < lessThan;
};
Utilicé un delegado anónimo allí como cierre (su sintaxis es un poco más clara y más cercana a C que el equivalente lambda), que capturó lessThan (una variable de pila) en el cierre. Cuando se evalúa el cierre, se seguirá haciendo referencia a lessThan (cuyo marco de pila puede haberse destruido). Si cambio menosThan, entonces cambio la comparación:
int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
return i < lessThan;
};
lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false
En C, esto sería ilegal:
BOOL (*lessThanTest)(int);
int lessThan = 100;
lessThanTest = &LessThan;
BOOL LessThan(int i) {
return i < lessThan; // compile error - lessThan is not in scope
}
aunque podría definir un puntero a función que toma 2 argumentos:
int lessThan = 100;
BOOL (*lessThanTest)(int, int);
lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false
BOOL LessThan(int i, int lessThan) {
return i < lessThan;
}
Pero, ahora tengo que pasar los 2 argumentos cuando lo evalúo. Si deseara pasar este puntero de función a otra función donde menos de lo que estaba en el alcance, tendría que mantenerla activa de forma manual pasándola a cada función de la cadena, o promoviéndola a nivel global.
Aunque la mayoría de los lenguajes principales que admiten cierres usan funciones anónimas, no hay ningún requisito para eso. Puede tener cierres sin funciones anónimas y funciones anónimas sin cierres.
Resumen: un cierre es una combinación de puntero de función + variables capturadas.
Una lambda es una función anónima, definida dinámicamente . No se puede hacer eso en C ... en cuanto a los cierres (o la convinación de los dos), el típico ejemplo de ceceo se vería algo así como:
(defun get-counter (n-start +-number)
"Returns a function that returns a number incremented
by +-number every time it is called"
(lambda () (setf n-start (+ +-number n-start))))
En términos de C, se podría decir que el entorno léxico (la pila) de get-counter
está siendo capturado por la función anónima y modificado internamente como se muestra en el siguiente ejemplo:
[1]> (defun get-counter (n-start +-number)
"Returns a function that returns a number incremented
by +-number every time it is called"
(lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]>