javascript - those - ¿Hay alguna razón por la que `this` se anule en el método` curry` de Crockford?
pronombres demostrativos en ingles wikipedia (4)
En el libro de Douglas Crockford "Javascript: The Good Parts", proporciona código para un método de curry
que toma una función y argumenta y devuelve esa función con los argumentos ya agregados (aparentemente, esto no es realmente lo que significa "curry" , pero es un ejemplo de "aplicación parcial" ). Aquí está el código, que he modificado para que funcione sin otro código personalizado que hizo:
Function.prototype.curry = function(){
var slice = Array.prototype.slice,
args = slice.apply(arguments),
that = this;
return function() {
// context set to null, which will cause `this` to refer to the window
return that.apply(null, args.concat(slice.apply(arguments)));
};
};
Así que si tienes una función de add
:
var add = function(num1, num2) {
return num1 + num2;
};
add(2, 4); // returns 6
Puedes hacer una nueva función que ya tenga un argumento:
var add1 = add.curry(1);
add1(2); // returns 3
Eso funciona bien. Pero lo que quiero saber es ¿por qué establece this
en null
? ¿No sería el comportamiento esperado que el método al curry sea el mismo que el original, incluido el mismo?
Mi versión de curry se vería así:
Function.prototype.myCurry = function(){
var slice = [].slice,
args = slice.apply(arguments),
that = this;
return function() {
// context set to whatever `this` is when myCurry is called
return that.apply(this, args.concat(slice.apply(arguments)));
};
};
Ejemplo
(Aquí hay un jsfiddle del ejemplo)
var calculator = {
history: [],
multiply: function(num1, num2){
this.history = this.history.concat([num1 + " * " + num2]);
return num1 * num2;
},
back: function(){
return this.history.pop();
}
};
var myCalc = Object.create(calculator);
myCalc.multiply(2, 3); // returns 6
myCalc.back(); // returns "2 * 3"
Si intento hacerlo a la manera de Douglas Crockford:
myCalc.multiplyPi = myCalc.multiply.curry(Math.PI);
myCalc.multiplyPi(1); // TypeError: Cannot call method ''concat'' of undefined
Si lo hago a mi manera:
myCalc.multiplyPi = myCalc.multiply.myCurry(Math.PI);
myCalc.multiplyPi(1); // returns 3.141592653589793
myCalc.back(); // returns "3.141592653589793 * 1"
Sin embargo, siento que si Douglas Crockford lo hizo a su manera, probablemente tenga una buena razón. ¿Qué me estoy perdiendo?
Pero lo que quiero saber es ¿por qué establece esto en nulo?
No hay realmente una razón. Probablemente quería simplificar, y la mayoría de las funciones que tienen sentido para ser aplicadas o aplicadas parcialmente no son métodos OOP que usen this
. En un estilo más funcional, la matriz de history
que se anexa sería otro parámetro de la función (y quizás incluso un valor de retorno).
¿No sería el comportamiento esperado que el método al curry sea el mismo que el original, incluido el mismo?
Sí, su implementación tiene mucho más sentido, sin embargo, uno no puede esperar que una función aplicada parcialmente aún deba ser llamada en el contexto correcto (como lo hace al reasignarla a su objeto) si la usa.
Para aquellos, puede echar un vistazo al bind de bind
de los objetos de Función para una aplicación parcial que incluye un valor específico de this
.
Razón 1: no es fácil proporcionar una solución general
El problema es que tu solución no es general. Si la persona que llama no asigna la nueva función a ningún objeto, o la asigna a un objeto completamente diferente, su función multiplyPi
dejará de funcionar:
var multiplyPi = myCalc.multiply.myCurry(Math.PI);
multiplyPi(1); // TypeError: this.history.concat is not a function
Por lo tanto, ni Crockford ni su solución pueden asegurar que la función se utilizará correctamente. Entonces puede ser más fácil decir que la función de curry
funciona solo en "funciones", no en "métodos", y establecer this
en null
para forzar eso. Aunque solo podríamos especular, ya que Crockford no menciona eso en el libro.
Razón 2 - Se explican las funciones.
Si pregunta "por qué Crockford no usó esto o aquello", la respuesta más probable es: "No fue importante en relación con el asunto demostrado". Crockford usa este ejemplo en el capítulo Funciones . El propósito del curry
del subcapítulo fue:
- Para mostrar que las funciones son objetos que puedes crear y manipular.
- Para demostrar otro uso de los cierres.
- Para mostrar cómo se pueden manipular los argumentos.
El ajuste de esto para un uso general con objetos no fue el propósito de este capítulo. Como es problemático, si no es que es imposible (ver Razón 1), fue más educativo ponerlo allí simplemente null
si poner algo que pudiera plantear preguntas si realmente funciona o no (aunque no ayudó en su caso :-) ).
Conclusión
Dicho esto, creo que puedes confiar perfectamente en tu solución! No hay ninguna razón particular en su caso para seguir la decisión de Crockfords de restablecer this
a null
. Sin embargo, debe tener en cuenta que su solución solo funciona bajo ciertas circunstancias y no está 100% limpia. Luego, la solución "orientada a objetos" limpia sería pedirle al objeto que cree un clon de su método dentro de sí mismo, para garantizar que el método resultante se mantenga dentro del mismo objeto.
Desde developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… :
thisArg El valor de esto proporcionado para la llamada a la diversión. Tenga en cuenta que este puede no ser el valor real visto por el método: si el método es una función en código de modo no estricto, nulo e indefinido se reemplazará con el objeto global, y los valores primitivos se encuadrarán.
Por lo tanto, si el método está en modo no estricto y el primer argumento es null
o undefined
está undefined
, this
de ese método hará referencia a la Window
. En modo estricto, esto es null
o undefined
. He añadido un ejemplo en vivo en este Fiddle .
Además, pasar null
o undefined
no hace ningún daño en caso de que la función no haga referencia a this
en absoluto. Probablemente por eso Crockford usó null
en su ejemplo para no complicar las cosas.
Lector cuidado, te espera un susto.
Hay mucho de qué hablar cuando se trata de curry, funciones, aplicación parcial y orientación a objetos en JavaScript. Trataré de mantener esta respuesta lo más breve posible, pero hay mucho que discutir. Por lo tanto, he estructurado mi artículo en varias secciones y al final de cada una he resumido cada sección para aquellos de ustedes que están demasiado impacientes para leerlo todo.
1. Curry o no curry.
Hablemos de Haskell. En Haskell todas las funciones se currifican por defecto. Por ejemplo, podríamos crear una función de add
en Haskell de la siguiente manera:
add :: Int -> Int -> Int
add a b = a + b
Observe el tipo de firma Int -> Int -> Int
? Significa que add
toma un Int
y devuelve una función de tipo Int -> Int
que a su vez toma un Int
y devuelve un Int
. Esto le permite aplicar parcialmente funciones en Haskell fácilmente:
add2 :: Int -> Int
add2 = add 2
La misma función en JavaScript se vería fea:
function add(a) {
return function (b) {
return a + b;
};
}
var add2 = add(2);
El problema aquí es que las funciones en JavaScript no están en curry por defecto. Necesitas currylas manualmente y eso es un dolor. Por lo tanto, utilizamos la aplicación parcial (aka aka) en su lugar.
Lección 1: el curry se utiliza para facilitar la aplicación parcial de funciones. Sin embargo, solo es efectivo en idiomas en los que las funciones se currifican de forma predeterminada (por ejemplo, Haskell). Si tiene que curry funciones manualmente, es mejor usar una aplicación parcial en su lugar.
2. La estructura de una función.
Las funciones no en curso también existen en Haskell. Parecen funciones en lenguajes de programación "normales":
main = print $ add(2, 3)
add :: (Int, Int) -> Int
add(a, b) = a + b
Puede convertir una función en su forma de curry a su forma no en curso y viceversa usando las funciones uncurry
y curry
en Haskell respectivamente. Una función no apurada en Haskell todavía toma solo un argumento. Sin embargo, ese argumento es un producto de múltiples valores (es decir, un tipo de producto ).
En la misma línea, las funciones en JavaScript también tienen un solo argumento (aún no lo saben). Ese argumento es un tipo de producto. El valor de los arguments
dentro de una función es una manifestación de ese tipo de producto. Esto se ejemplifica mediante el método de apply
en JavaScript que toma un tipo de producto y le aplica una función. Por ejemplo:
print(add.apply(null, [2, 3]));
¿Puedes ver la similitud entre la línea anterior en JavaScript y la siguiente línea en Haskell?
main = print $ add(2, 3)
Ignora la tarea main
si no sabes para qué sirve. Es irrelevante en relación con el tema en cuestión. Lo importante es que la tupla (2, 3)
en Haskell es isomorfa a la matriz [2, 3]
en JavaScript. ¿Qué aprendemos de esto?
La función de apply
en JavaScript es la misma que la aplicación de función (o $
) en Haskell:
($) :: (a -> b) -> a -> b
f $ a = f a
Tomamos una función de tipo a -> b
y la aplicamos a un valor de tipo a
para obtener un valor de tipo b
. Sin embargo, dado que todas las funciones en JavaScript no están en funcionamiento de forma predeterminada, la función de apply
siempre toma un tipo de producto (es decir, una matriz) como segundo argumento. Es decir, el valor del tipo a
es en realidad un tipo de producto en JavaScript.
Lección 2: Todas las funciones en JavaScript solo toman un único argumento que es un tipo de producto (es decir, el valor de los arguments
). Si esto fue pensado o casualidad es una cuestión de especulación. Sin embargo, el punto importante es que entiendes que matemáticamente cada función solo toma un solo argumento.
Matemáticamente, una función se define como un morphism : a -> b
. Toma un valor de tipo a
y devuelve un valor de tipo b
. Un morfismo solo puede tener un argumento. Si quieres múltiples argumentos entonces puedes:
- Devolver otro morfismo (es decir,
b
es otro morfismo). Esto es curry. Haskell hace esto. - Defina
a
para ser un producto de varios tipos (es decir,a
es un tipo de producto). JavaScript hace esto.
De las dos, prefiero las funciones al curry, ya que hacen que la aplicación parcial sea trivial. La aplicación parcial de funciones "sin prisa" es más complicada. No es difícil, fíjate, pero es más complicado. Esta es una de las razones por las que me gusta Haskell más que JavaScript: las funciones están actualizadas de forma predeterminada.
3. ¿Por qué no importa OOP?
Echemos un vistazo a algunos códigos orientados a objetos en JavaScript. Por ejemplo:
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filter(odd).length;
function odd(n) {
return n % 2 !== 0;
}
Ahora usted podría preguntarse cómo está orientado este objeto. Se parece más al código funcional. Después de todo, podrías hacer lo mismo en Haskell:
oddities = length . filter odd $ [0..9]
Sin embargo, el código anterior está orientado a objetos. El literal de matriz es un objeto que tiene un filter
método que devuelve un nuevo objeto de matriz. Luego simplemente accedemos a la length
del nuevo objeto de matriz.
¿Qué aprendemos de esto? El encadenamiento de operaciones en lenguajes orientados a objetos es lo mismo que componer funciones en lenguajes funcionales. La única diferencia es que el código funcional lee hacia atrás. Vamos a ver por qué.
En JavaScript this
parámetro es especial. Es independiente de los parámetros formales de la función, por lo que debe especificar un valor por separado en el método de apply
. Debido a que this
viene antes de los parámetros formales, los métodos se encadenan de izquierda a derecha.
add.apply(null, [2, 3]); // this comes before the formal parameters
Si this
fuera después de los parámetros formales, el código anterior probablemente se leería como:
var oddities = length.filter(odd).[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
apply([2, 3], null).add; // this comes after the formal parameters
No es muy bonito, ¿verdad? Entonces, ¿por qué las funciones en Haskell leen al revés? La respuesta es curry. Las funciones de Haskell también tienen un parámetro " this
". Sin embargo, a diferencia de JavaScript, this
parámetro en Haskell no es especial. Además viene al final de la lista de argumentos. Por ejemplo:
filter :: (a -> Bool) -> [a] -> [a]
La función de filter
toma una función de predicado y this
lista y devuelve una nueva lista con solo los elementos filtrados. Entonces, ¿por qué es this
parámetro el último? Facilita la aplicación parcial. Por ejemplo:
filterOdd = filter odd
oddities = length . filterOdd $ [0..9]
En JavaScript escribirías:
Array.prototype.filterOdd = [].filter.myCurry(odd);
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filterOdd().length;
Ahora, ¿cuál elegirías? Si todavía te estás quejando por leer al revés, entonces tengo noticias para ti. Puede hacer que el código de Haskell se lea hacia delante utilizando "aplicación hacia atrás" y "composición hacia atrás" de la siguiente manera:
($>) :: a -> (a -> b) -> b
a $> f = f a
(>>>) :: (a -> b) -> (b -> c) -> (a -> c)
f >>> g = g . f
oddities = [0..9] $> filter odd >>> length
Ahora tienes lo mejor de ambos mundos. Su código se lee hacia adelante y usted obtiene todos los beneficios del curry.
Hay muchos problemas con this
que no ocurren en lenguajes funcionales:
-
this
parámetro es especializado. A diferencia de otros parámetros, no puede simplemente establecerlo en un objeto arbitrario. Por lo tanto, debe utilizar lacall
para especificar un valor diferente parathis
. - Si desea aplicar parcialmente funciones en JavaScript, entonces necesita especificar
null
como el primer parámetro debind
. Del mismo modo paracall
yapply
.
La programación orientada a objetos no tiene nada que ver con this
. De hecho, también puede escribir código orientado a objetos en Haskell. Me atrevería a decir que Haskell es de hecho un lenguaje de programación orientado a objetos, y mucho mejor que Java o C ++.
Lección 3: Los lenguajes de programación funcionales están más orientados a objetos que la mayoría de los lenguajes de programación orientados a objetos. De hecho, el código orientado a objetos en JavaScript sería mejor (aunque se admite que es menos legible) si se escribe en un estilo funcional.
El problema con el código orientado a objetos en JavaScript es this
parámetro. En mi humilde opinión, this
parámetro no debería tratarse de manera diferente a los parámetros formales (Lua lo entendió bien). El problema con this
es que:
- No hay manera de establecer
this
como otros parámetros formales. Tienes que usar lacall
lugar. - Debe establecer
this
ennull
en elbind
si desea aplicar solo parcialmente una función.
Como nota al margen, me di cuenta de que cada sección de este artículo se está volviendo más larga que la sección anterior. Por lo tanto, prometo mantener la siguiente (y última) sección lo más corta posible.
4. En defensa de Douglas Crockford.
A estas alturas, debes haber comprendido que creo que la mayoría de JavaScript está dañado y que deberías cambiarte a Haskell. Me gusta creer que Douglas Crockford también es un programador funcional y que está tratando de arreglar JavaScript.
¿Cómo sé que es un programador funcional? Él es el chico que:
- Popularizó el equivalente funcional de la
new
palabra clave (tambiénObject.create
comoObject.create
). Si aún no lo ha hecho, debe dejar de usar lanew
palabra clave . - Se intentó explicar el concepto de mónadas y gónadas a la comunidad de JavaScript.
De todos modos, creo que Crockford anuló this
en la función de curry
porque sabe cuán malo es this
. Sería un sacrilegio establecerlo en otra cosa que no sea null
en un libro titulado "JavaScript: The Good Parts". Creo que está haciendo del mundo un lugar mejor, una característica a la vez.
Al anular this
Crockford te obliga a dejar de confiar en él.
Edición: como Bergi solicitó, describiré una forma más funcional de escribir su código de Calculator
orientado a objetos. Utilizaremos el método de curry
de Crockford. Comencemos con las funciones de multiply
y back
:
function multiply(a, b, history) {
return [a * b, [a + " * " + b].concat(history)];
}
function back(history) {
return [history[0], history.slice(1)];
}
Como puede ver, las funciones de multiply
y back
no pertenecen a ningún objeto. Por lo tanto puedes usarlos en cualquier matriz. En particular, su clase de Calculator
es solo una envoltura para la lista de cadenas. Por lo tanto, ni siquiera necesita crear un tipo de datos diferente para ello. Por lo tanto:
var myCalc = [];
Ahora puedes usar el método de curry
de Crockford para una aplicación parcial:
var multiplyPi = multiply.curry(Math.PI);
A continuación, crearemos una función de test
para multiplyPi
por uno y para volver al estado anterior:
var test = bindState(multiplyPi.curry(1), function (prod) {
alert(prod);
return back;
});
Si no te gusta la sintaxis, entonces puedes cambiar a LiveScript :
test = do
prod <- bindState multiplyPi.curry 1
alert prod
back
La función bindState
es la función de bind
de la mónada de estado. Se define de la siguiente manera:
function bindState(g, f) {
return function (s) {
var a = g(s);
return f(a[0])(a[1]);
};
}
Así que vamos a ponerlo a prueba:
alert(test(myCalc)[0]);
Vea la demostración aquí: http://jsfiddle.net/5h5R9/
Por cierto, todo este programa hubiera sido más breve si se escribiera en LiveScript de la siguiente manera:
multiply = (a, b, history) --> [a * b, [a + " * " + b] ++ history]
back = ([top, ...history]) -> [top, history]
myCalc = []
multiplyPi = multiply Math.PI
bindState = (g, f, s) -->
[a, t] = g s
(f a) t
test = do
prod <- bindState multiplyPi 1
alert prod
back
alert (test myCalc .0)
Vea la demostración del código compilado de LiveScript: http://jsfiddle.net/5h5R9/1/
Entonces, ¿cómo se orienta este objeto de código? Wikipedia define la programación orientada a objetos como:
La programación orientada a objetos (OOP) es un paradigma de programación que representa conceptos como "objetos" que tienen campos de datos (atributos que describen el objeto) y procedimientos asociados conocidos como métodos. Los objetos, que suelen ser instancias de clases, se utilizan para interactuar entre sí para diseñar aplicaciones y programas informáticos.
Según esta definición, los lenguajes de programación funcionales como Haskell están orientados a objetos porque:
- En Haskell, representamos conceptos como tipos de datos algebraicos que son esencialmente "objetos con esteroides". Un ADT tiene uno o más constructores que pueden tener cero o más campos de datos.
- Los ADT en Haskell tienen funciones asociadas. Sin embargo, a diferencia de los lenguajes de programación orientados a objetos convencionales, los ADT no son dueños de las funciones. En cambio, las funciones se especializan en los TDA. Esto es realmente bueno ya que los ADT están abiertos a agregar más métodos. En lenguajes tradicionales OOP como Java y C ++ están cerrados.
- Los ADTs pueden ser instancias de clases de tipos que son similares a las interfaces en Java. Por lo tanto, todavía tiene herencia, varianza y polimorfismo de subtipo, pero en una forma mucho menos intrusiva. Por ejemplo,
Functor
es una superclase deApplicative
.
El código anterior también está orientado a objetos. El objeto en este caso es myCalc
que es simplemente una matriz. Tiene dos funciones asociadas con él: multiply
y back
. Sin embargo no posee estas funciones. Como puede ver, el código orientado a objetos "funcional" tiene las siguientes ventajas:
- Los objetos no poseen métodos. Por lo tanto, es fácil asociar nuevas funciones a objetos.
- La aplicación parcial se hace simple a través de currying.
- Promueve la programación genérica.
Así que espero que haya ayudado.