Punteros de función, cierres y Lambda

11 minutos de lectura

avatar de usuario
Ninguna

Ahora estoy aprendiendo sobre los punteros 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 incorrecta 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 de 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 de la función aún apunta a una función definida (nombrada) en lugar de la práctica de definir la función de forma anónima.

¿Por qué pasar una función a otra función se considera más poderoso en el segundo caso, donde no tiene nombre, que en el primero, 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.

avatar de usuario
marca corchetes

Una lambda (o cierre) encapsula tanto el puntero de función como las variables. Por eso, en C#, puedes hacer:

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

Usé 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 haber sido destruido). Si cambio lessThan, 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 de 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 quisiera pasar este puntero de función a otra función en la que lessThan no estuviera dentro del alcance, tendría que mantenerlo activo manualmente pasándolo a cada función de la cadena o promoviéndolo a global.

Aunque la mayoría de los lenguajes principales que admiten cierres usan funciones anónimas, no hay requisitos 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.

  • gracias, realmente condujiste a casa la idea a la que otras personas intentaban llegar.

    – Ninguna

    16 de octubre de 2008 a las 15:14

  • Probablemente estaba usando una versión anterior de C cuando escribió esto o no recordó reenviar declarar la función, pero no observo el mismo comportamiento que mencionó cuando pruebo esto. ideone.com/JsDVBK

    – smac89

    8 de noviembre de 2016 a las 20:47


  • @ smac89: hizo que la variable lessThan sea global; lo mencioné explícitamente como una alternativa.

    – Mark Brackett

    9 de noviembre de 2016 a las 14:01

Como alguien que ha escrito compiladores para lenguajes con y sin cierres ‘reales’, discrepo respetuosamente con algunas de las respuestas anteriores. Un cierre de Lisp, Scheme, ML o Haskell no crea una nueva función dinámicamente. en cambio, reutiliza una función existente pero lo hace con nuevas variables libres. La colección de variables libres a menudo se llama el ambienteal menos por los teóricos del lenguaje de programación.

Un cierre es solo un agregado que contiene una función y un entorno. En el compilador Standard ML of New Jersey, representamos uno como un 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 contenga un puntero al mismo código, pero con diferente valores de las variables libres.

Puedes simular todo esto en C, pero es un fastidio. Dos técnicas son populares:

  1. 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 en dos variables C.

  2. 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 #1 es ideal cuando intenta simular algún tipo de polimorfismo en C y no desea revelar el tipo de entorno; utiliza un puntero void* para representar el entorno. Por ejemplo, mire el de Dave Hanson Interfaces e implementaciones de C. La técnica #2, que se parece más a lo que sucede en los compiladores de código nativo para lenguajes funcionales, también se parece a otra técnica familiar… Objetos C++ con funciones miembro 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 tendrían los cierres de funciones en la programación eficiente del futuro. Luego sucedió la revolución de la ‘programación orientada a objetos’, y ahora todos programan usando cierres de funciones, excepto que todavía se niegan a llamarlos así.

  • +1 para la explicación y la cita de que OOP es realmente cierres — reutiliza una función existente pero lo hace con nuevas variables libres — funciones (métodos) que toman el entorno (un puntero de estructura a datos de instancia de objeto que no son más que nuevos estados) para operar.

    – leyendas2k

    26 de junio de 2014 a las 14:25


avatar de usuario
Hermes

En C no puede definir la función en línea, por lo que realmente no puede crear un cierre. Todo lo que está haciendo es pasar una referencia a algún método predefinido. En lenguajes que admiten métodos/cierres anónimos, la definición de los métodos es mucho más flexible.

En los términos más simples, los punteros de función no tienen un alcance asociado con ellos (a menos que cuente el alcance global), mientras que los cierres incluyen el alcance del método que los define. Con lambdas, puede escribir un método que escriba un método. Los cierres le permiten vincular “algunos argumentos a una función y obtener como resultado una función de menor aridad”. (tomado del comentario de Thomas). No puedes hacer eso en C.

EDITAR: agregar un ejemplo (voy a usar la sintaxis de Actionscript-ish porque eso es lo que tengo en mente en este momento):

Digamos 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 llama. Como, digamos, algún método que cause un retraso antes de ejecutar el método que le pasaste (ejemplo estúpido, pero quiero que sea simple).

function runLater(f:Function):Void {
  sleep(100);
  f();
}

Ahora diga que desea que el usuario runLater() retrase 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 process() 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 ámbito global.

Espero que tenga sentido.

  • Modifiqué mi respuesta según tu comentario. Todavía no estoy 100% claro sobre los detalles de los términos, así que solo lo cité directamente. 🙂

    – Hermes

    16 de octubre de 2008 a las 15:05

  • La capacidad en línea de las funciones anónimas es un detalle de implementación de (¿la mayoría?) los principales lenguajes de programación; no es un requisito para los cierres.

    – Mark Brackett

    16 de octubre de 2008 a las 15:16

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 obtener 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, mostrando cómo los cierres facilitan las cosas.

En C, los punteros de función se pueden pasar como argumentos a funciones y devolverse como valores de funciones, pero las funciones existen solo en el nivel superior: no se pueden anidar definiciones de funciones entre sí. Piense en lo que se necesitaría para que C admitiera funciones anidadas que puedan acceder a las variables de la función externa, y al mismo tiempo poder 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 a funciones en C y en la mayoría de los lenguajes similares: navegue por el pila de llamadas entrada en Wikipedia.)

¿Qué tipo de objeto es un puntero a una función anidada? No puede ser solo 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 recursividad, puede haber varias llamadas diferentes de la función externa activas al mismo tiempo). Esto se llama problema funargy hay dos subproblemas: el problema de funargs hacia abajo y el problema de funargs hacia arriba.

El problema de funargs hacia abajo, es decir, enviar un puntero de función “abajo de la pila” como argumento para una función que llama, en realidad no es incompatible con C y GCC apoya funciones anidadas como funargs hacia abajo. En GCC, cuando crea un puntero a una función anidada, realmente obtiene un puntero a una trampolínuna pieza de código construida dinámicamente que configura el puntero de enlace estático y luego llama a la función real, que usa el puntero de enlace estático para acceder a las variables de la función externa.

El problema de funargs hacia arriba 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 luego el puntero de enlace estático podría apuntar a 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 represente una función anidada solo apunte al registro de activación de la función externa. Tal objeto se llama cierre. Entonces, el idioma normalmente tendrá que admitir recolección de basura para que los registros se puedan liberar una vez que no haya más punteros apuntando a ellos.

lambdas (funciones anónimas) son realmente un tema aparte, pero por lo general 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.

Una lambda es una anónima, definida dinámicamente función. Simplemente no puede hacer eso en C… en cuanto a los cierres (o la convinación de los dos), el ejemplo típico 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 muestra 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]> 

avatar de usuario
andy abolladura

Los cierres implican que alguna variable desde el punto de definición de la función está vinculada con la lógica de la función, como poder declarar un miniobjeto 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 las apuntaba. Esto conduciría al tipo de errores que la gente obtiene cuando devuelven los punteros a las variables locales por descuido. Los cierres básicamente implican que todas las variables relevantes son elementos contados por referencia o recolectados como elementos no utilizados en un montón.

No me siento cómodo equiparando lambda con el 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?).

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad