ramdajs ramda programming functional book javascript functional-programming ramda.js lifting

javascript - programming - github ramdajs



No puedo envolver mi cabeza alrededor de "lift" en Ramda.js (3)

Buscando en la fuente de Ramda.js, específicamente en la función de "elevación".

lift

liftN

Aquí está el ejemplo dado:

var madd3 = R.lift(R.curry((a, b, c) => a + b + c)); madd3([1,2,3], [1,2,3], [1]); //=> [3, 4, 5, 4, 5, 6, 5, 6, 7]

Entonces, el primer número del resultado es fácil, a , b y c , son todos los primeros elementos de cada matriz. El segundo no es tan fácil de entender. ¿Son los argumentos el segundo valor de cada matriz (2, 2, indefinido) o es el segundo valor de la primera matriz y los primeros valores de la segunda y la tercera matriz?

Incluso sin tener en cuenta el orden de lo que está sucediendo aquí, realmente no veo el valor. Si ejecuto esto sin lift primero, terminaré con las matrices concatenadas como cadenas. Esto parece estar funcionando como flatMap pero no puedo seguir la lógica detrás de él.


Gracias a las respuestas de Scott Sauyet y Bergi, envolví mi cabeza alrededor de esto. Al hacerlo, sentí que todavía había aros para saltar y juntar todas las piezas. Documentaré algunas de las preguntas que tuve en el viaje, espero que pueda ser de ayuda para algunos.

Aquí está el ejemplo de R.lift que intentamos entender:

var madd3 = R.lift((a, b, c) => a + b + c); madd3([1,2,3], [1,2,3], [1]); //=> [3, 4, 5, 4, 5, 6, 5, 6, 7]

Para mí, hay tres preguntas que deben responderse antes de entenderlo .

  1. La especificación Apply Fantasy-land (me referiré a ella como Apply ) y lo que hace Apply#ap
  2. La implementación R.ap de R.ap y qué tiene que ver Array con la especificación Apply
  3. ¿Qué papel juega el curry en R.lift

Entendiendo la especificación de Apply

En fantasy-land, un objeto implementa Apply spec cuando tiene un método ap definido (ese objeto también tiene que implementar la especificación de Functor definiendo un método de map ).

El método ap tiene la siguiente firma:

ap :: Apply f => f a ~> f (a -> b) -> f b

En la notación de firma tipo fantasía-tierra :

  • => declara restricciones de tipo, por lo que f en la firma anterior se refiere al tipo Apply
  • ~> declara declaración de método , por lo que ap debería ser una función declarada en Apply que envuelve un valor al que nos referimos como (veremos en el siguiente ejemplo, algunas implementaciones de ap de fantasy-land no son consistentes con esta firma, pero la idea es la misma

Digamos que tenemos dos objetos v y u ( v = fa; u = f (a -> b) ) por lo tanto, esta expresión es válida v.ap(u) , algunas cosas que hay que notar aquí:

  • v y u implementan Apply . v tiene un valor, u tiene una función pero tienen la misma ''interfaz'' de Apply (esto ayudará a entender la siguiente sección, cuando se trata de R.ap y Array )
  • El valor a y la función a -> b ignoran Apply , la función simplemente transforma el valor a . Es la Apply que pone el valor y la función dentro del contenedor y la aplicación que los extrae, invoca la función en el valor y los vuelve a colocar.

Entendiendo el Ramda de R.ap

La firma de R.ap tiene dos casos:

  1. Apply f => f (a → b) → fa → fb : Esto es muy similar a la firma de Apply#ap en la última sección, la diferencia es cómo se invoca ap ( Apply#ap vs. R.ap ) y el orden de params.
  2. [a → b] → [a] → [b] : esta es la versión si reemplazamos Apply f con Array , ¿recuerda que el valor y la función deben estar ajustados en el mismo contenedor en la sección anterior? Es por eso que al usar R.ap con Array s, el primer argumento es una lista de funciones, incluso si desea aplicar solo una función, póngala en un Array.

Veamos un ejemplo, estoy usando Maybe de ramada-fantasy , que implementa Apply , una inconsistencia aquí es que la firma de Maybe#ap es: ap :: Apply f => f (a -> b) ~> fa -> fb . Parece que otras implementaciones de fantasy-land también siguen esto, sin embargo, no debería afectar nuestra comprensión:

const R = require(''ramda''); const Maybe = require(''ramda-fantasy'').Maybe; const a = Maybe.of(2); const plus3 = Maybe.of(x => x + 3); const b = plus3.ap(a); // invoke Apply#ap const b2 = R.ap(plus3, a); // invoke R.ap console.log(b); // Just { value: 5 } console.log(b2); // Just { value: 5 }

Entendiendo el ejemplo de R.lift

En el ejemplo de R.lift con matrices, una función con aridad de 3 se pasa a R.lift : var madd3 = R.lift((a, b, c) => a + b + c); , ¿cómo funciona con las tres matrices [1, 2, 3], [1, 2, 3], [1] ? También tenga en cuenta que no está al curry.

En realidad, dentro del código fuente de R.liftN (al que R.lift delega), la función que se pasa es auto-curry , luego itera a través de los valores (en nuestro caso, tres arrays), reduciendo a un resultado: en cada iteración invoca ap con la función de curry y un valor (en nuestro caso, una matriz). Es difícil de explicar con palabras, veamos el equivalente en código:

const R = require(''ramda''); const Maybe = require(''ramda-fantasy'').Maybe; const madd3 = (x, y, z) => x + y + z; // example from R.lift const result = R.lift(madd3)([1, 2, 3], [1, 2, 3], [1]); // this is equivalent of the calculation of ''result'' above, // R.liftN uses reduce, but the idea is the same const result2 = R.ap(R.ap(R.ap([R.curry(madd3)], [1, 2, 3]), [1, 2, 3]), [1]); console.log(result); // [ 3, 4, 5, 4, 5, 6, 5, 6, 7 ] console.log(result2); // [ 3, 4, 5, 4, 5, 6, 5, 6, 7 ]

Una vez que se entienda la expresión del resultado calculador2, el ejemplo quedará claro.

Aquí hay otro ejemplo, usando R.lift en Apply :

const R = require(''ramda''); const Maybe = require(''ramda-fantasy'').Maybe; const madd3 = (x, y, z) => x + y + z; const madd3Curried = Maybe.of(R.curry(madd3)); const a = Maybe.of(1); const b = Maybe.of(2); const c = Maybe.of(3); const sumResult = madd3Curried.ap(a).ap(b).ap(c); // invoke #ap on Apply const sumResult2 = R.ap(R.ap(R.ap(madd3Curried, a), b), c); // invoke R.ap const sumResult3 = R.lift(madd3)(a, b, c); // invoke R.lift, madd3 is auto-curried console.log(sumResult); // Just { value: 6 } console.log(sumResult2); // Just { value: 6 } console.log(sumResult3); // Just { value: 6 }

Un mejor ejemplo sugerido por Scott Sauyet en los comentarios (proporciona bastante información, sugiero que los lea) sería más fácil de entender, al menos indica al lector la dirección en la que R.lift calcula el producto cartesiano para Array s.

var madd3 = R.lift((a, b, c) => a + b + c); madd3([100, 200], [30, 40, 50], [6, 7]); //=> [136, 137, 146, 147, 156, 157, 236, 237, 246, 247, 256, 257]

Espero que esto ayude.


La respuesta de Bergi es genial. Pero otra forma de pensar acerca de esto es ser un poco más específico. Ramda realmente necesita incluir un ejemplo que no sea de lista en su documentación, ya que las listas realmente no capturan esto.

Tomemos una función simple:

var add3 = (a, b, c) => a + b + c;

Esto funciona en tres números. Pero, ¿y si tuvieras contenedores con números? Tal vez tenemos Maybe s. No podemos simplemente juntarlos:

const Just = Maybe.Just, Nothing = Maybe.Nothing; add3(Just(10), Just(15), Just(17)); //=> ERROR!

(Ok, esto es Javascript, en realidad no lanzará un error aquí, solo intenta concatenar algo que no debería ... ¡pero definitivamente no hace lo que quieres!)

Si pudiéramos elevar esa función hasta el nivel de los contenedores, nos haría la vida más fácil. Lo que Bergi señaló como lift3 se implementa en Ramda con liftN(3, fn) , y un brillo, lift(fn) que simplemente utiliza la aridad de la función suministrada. Entonces, podemos hacer:

const madd3 = R.lift(add3); madd3(Just(10), Just(15), Just(17)); //=> Just(42) madd3(Just(10), Nothing(), Just(17)); //=> Nothing()

Pero esta función elevada no sabe nada específico sobre nuestros contenedores, solo que implementan ap . Ramda implementa ap para listas de una manera similar a la aplicación de la función a las tuplas en el producto cruzado de las listas, por lo que también podemos hacer esto:

madd3([100, 200], [30, 40], [5, 6, 7]); //=> [135, 136, 137, 145, 146, 147, 235, 236, 237, 245, 246, 247]

Así es como pienso en el lift . Toma una función que funciona al nivel de algunos valores y la eleva a una función que funciona al nivel de los contenedores de esos valores.


lift / liftN "eleva" una función ordinaria en un contexto aplicativo.

// lift1 :: (a -> b) -> f a -> f b // lift1 :: (a -> b) -> [a] -> [b] function lift1(fn) { return function(a_x) { return R.ap([fn], a_x); } }

Ahora, el tipo de ap ( f (a->b) -> fa -> fb ) tampoco es fácil de entender, pero el ejemplo de la lista debe ser comprensible.

Lo interesante aquí es que usted pasa en una lista y recupera una lista, por lo que puede aplicar esto repetidamente siempre que las funciones en la primera lista tengan el tipo correcto:

// lift2 :: (a -> b -> c) -> f a -> f b -> f c // lift2 :: (a -> b -> c) -> [a] -> [b] -> [c] function lift2(fn) { return function(a_x, a_y) { return R.ap(R.ap([fn], a_x), a_y); } }

Y lift3 , que implícitamente lift3 en tu ejemplo, funciona igual - ahora con ap(ap(ap([fn], a_x), a_y), a_z) .