oop - que - closures go
Cierres: ¿por qué son tan útiles? (10)
Como desarrollador de OO , tal vez tenga dificultades para ver su valor. ¿Qué valor agregado dan? ¿Encajan en un mundo OO?
Aquí hay algunos artículos interesantes:
- Cruzando fronteras: cierres - por Bruce Tate, menciona algunas ventajas de los cierres
- Una definición de cierres - por Neal Gafter (una de las personas que inventó una de las propuestas de cierres para Java)
En cuanto a la consulta final: "¿Los cierres encajan en un mundo OO?"
En muchos idiomas, los cierres, junto con las funciones de primera clase, proporcionan la base para el diseño de programas OO: Javascript, Lua y Perl vienen a la mente.
En otro lenguaje de OO más "tradicional" con el que estoy familiarizado, Java, hay 2 diferencias principales:
- Las funciones no son construcciones de primera clase
- En Java, los detalles de implementación de OO (si los cierres se usan internamente) no se exponen directamente al desarrollador, ya que están en JavaScript, Lua y Perl.
Por lo tanto, en un lenguaje de "OO tradicional" como Java, la adición de cierres es en gran medida azúcar tanto sintáctica.
En mi humilde opinión, se reduce a la capacidad de capturar bloques de código y su contexto para referenciarlos en algún momento posterior y ejecutarlos cuando / if / según sea necesario.
Puede que no parezcan ser un gran problema, y los cierres definitivamente no son algo que necesites para hacer las cosas a diario, pero pueden hacer que el código de código sea mucho más simple y más limpio para escribir / administrar.
[editar - Ejemplo de código basado en el comentario anterior]
Java:
List<Integer> numbers = ...;
List<Integer> positives = new LinkedList<Integer>();
for (Integer number : integers) {
if (number >= 0) {
positives.add(number);
}
}
Scala (omitiendo algunas de las otras sutilezas como inferencia tipográfica y comodines, así que solo estamos comparando el efecto del cierre):
val numbers:List[Int] = ...
val positives:List[Int] = numbers.filter(i:Int => i >= 0)
Es una lástima, la gente ya no aprende Smalltalk en la educación; allí, los cierres se usan para estructuras de control, devoluciones de llamadas, enumeración de colecciones, manejo de excepciones y más. Para un buen pequeño ejemplo, aquí hay un hilo del manejador de acción de cola de trabajo (en Smalltalk):
|actionQueue|
actionQueue := SharedQueue new.
[
[
|a|
a := actionQueue next.
a value.
] loop
] fork.
actionQueue add: [ Stdout show: 1000 factorial ].
y, para aquellos que no pueden leer Smalltalk, lo mismo en la sintaxis de JavaScript:
var actionQueue;
actionQueue = new SharedQueue;
function () {
for (;;) {
var a;
a = actionQueue.next();
a();
};
}.fork();
actionQueue.add( function () { Stdout.show( 1000.factorial()); });
(bueno, como veis: la sintaxis ayuda a leer el código)
Editar: observe cómo se hace referencia a actionQueue desde el interior de los bloques, que funciona incluso para el bloque de hilos bifurcado. Eso es lo que hace que los cierres sean tan fáciles de usar.
Los cierres encajan bastante bien en un mundo OO.
Como ejemplo, considere C # 3.0: tiene cierres y muchos otros aspectos funcionales, pero sigue siendo un lenguaje muy orientado a objetos.
En mi experiencia, los aspectos funcionales de C # tienden a permanecer dentro de la implementación de los miembros de la clase, y no tanto como parte de la API pública que mis objetos terminan exponiendo.
Como tal, el uso de cierres tiende a ser detalles de implementación en otro código orientado a objetos.
Los uso todo el tiempo, ya que este fragmento de código de una de nuestras pruebas unitarias (contra Moq ) muestra:
var typeName = configuration.GetType().AssemblyQualifiedName;
var activationServiceMock = new Mock<ActivationService>();
activationServiceMock.Setup(s => s.CreateInstance<SerializableConfigurationSection>(typeName)).Returns(configuration).Verifiable();
Hubiera sido bastante difícil especificar el valor de entrada (typeName) como parte de la expectativa de Mock si no hubiera sido por la característica de cierre de C #.
Los cierres no te dan ningún poder extra.
Cualquier cosa que puedas lograr con ellos puedes lograrlo sin ellos.
Pero son muy útiles para hacer que el código sea más claro y legible. Y como todos sabemos, el código abreviado legible es un código que es más fácil de depurar y contiene menos errores.
Déjame darte un pequeño ejemplo de uso posible de Java:
button.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) {
System.out.println("Pressed");
}
});
Sería reemplazado (si Java tuviera cierres) con:
button.addActionListener( { System.out.println("Pressed"); } );
Para mí, el mayor beneficio de los cierres es cuando estás escribiendo un código que inicia una tarea, deja la tarea para ejecutarse y especifica lo que debería suceder cuando se realiza la tarea. En general, el código que se ejecuta al final de la tarea necesita acceder a los datos que están disponibles al principio, y los cierres lo hacen fácil.
Por ejemplo, un uso común en JavaScript es iniciar una solicitud HTTP. Quien lo esté comenzando probablemente quiera controlar lo que sucede cuando llega la respuesta. Entonces harás algo como esto:
function sendRequest() {
var requestID = "123";
Ext.Ajax.request({
url:''/myUrl''
success: function(response) {
alert("Request " + requestID + " returned");
}
});
}
Debido a los cierres de JavaScript, la variable "requestID" se captura dentro de la función de éxito. Esto muestra cómo puede escribir las funciones de solicitud y respuesta en el mismo lugar y compartir variables entre ellas. Sin cierres, tendría que pasar in requestID como argumento, o crear un objeto que contenga requestID y la función.
Puedes verlo como una generalización de una clase.
Tu clase tiene algo de estado. Tiene algunas variables miembro que sus métodos pueden usar.
Un cierre es simplemente una forma más conveniente de dar a una función acceso al estado local.
En lugar de tener que crear una clase que conozca la variable local que desea que use la función, simplemente puede definir la función en el lugar, y puede acceder implícitamente a cada variable que esté actualmente visible.
Cuando define un método miembro en un lenguaje OOP tradicional, su cierre es "todos los miembros visibles en esta clase".
Los lenguajes con soporte de cierre "adecuado" simplemente generalizan esto, por lo que el cierre de una función es "todas las variables visibles aquí". Si " aquí " es una clase, entonces tienes un método de clase tradicional.
Si " aquí " está dentro de otra función, entonces tienes lo que los programadores funcionales consideran como un cierre. Su función ahora puede acceder a cualquier elemento visible en la función principal.
Por lo tanto, es solo una generalización, eliminando la absurda restricción de que "las funciones solo se pueden definir dentro de las clases", pero manteniendo la idea de que "las funciones pueden ver las variables visibles en el punto donde están declaradas".
Quizás en el mundo de la programación compilada, los beneficios de los cierres son menos notorios. En JavaScript, los cierres son increíblemente poderosos. Esto es por dos razones:
1) JavaScript es un lenguaje interpretado, por lo que la eficiencia de la instrucción y la conservación del espacio de nombres son increíblemente importantes para una ejecución más rápida y más receptiva de programas o programas grandes que evalúan grandes cantidades de datos.
2) JavaScript es un lenguaje lambda. Esto significa que las funciones de JavaScript son objetos de primera clase que definen el alcance y el alcance de una función primaria accesible para objetos secundarios. Esto es importante ya que una variable puede declararse en una función principal, usarse en una función secundaria y retener el valor incluso después de que la función secundaria regrese. Eso significa que una variable puede reutilizar una variable muchas veces con un valor ya definido por la última iteración de esa función.
Como resultado, los cierres son increíblemente potentes y proporcionan una eficacia superior en JavaScript.
mirando los ejemplos anteriores, puedo agregar mi parte.
Aprecio el estilo, la parte que oculta el estado y el encadenamiento de la expresión, pero esto no es suficiente
Se puede decir mucho sobre el código simple y explícito
- Siento que Scala, Perl y algunos otros lenguajes tipo Scheme están diseñados para hacer una especie de taquigrafía de la manera particular de pensar del diseñador del lenguaje o hacerlos "más productivos"
Tomaría la simplicidad de Python y la sintaxis temprana de Java, etc. como una mejor forma de simplicidad y explicitud
Puede ser bastante rápido escribir cierres encadenados crípticos en un espacio reducido. sin embargo, todo se puede hacer mediante objetos simples y algoritmos
Se necesitan cierres, pero no tan a menudo para garantizar que sean ciudadanos de primera clase.