javascript functional-programming monads es6-promise functor

javascript - ¿Por qué son promesas mónadas?



functional-programming monads (5)

He estado aprendiendo sobre programación funcional y me he encontrado con mónadas, functores y aplicantes.

Según tengo entendido, se aplican las siguientes definiciones:

a) (A => B) => C [A] => C [B] | Functor

b) (A => C [B]) => C [A] => C [B] | Monada

c) (C [A => B]) => C [A] => C [B] | Aplicativo

(referencia: https://thedet.wordpress.com/2012/04/28/functors-monads-applicatives-can-be-so-simple/ )

Además, entiendo que una mónada es un caso especial de un Functor. Como en, aplica una función que devuelve un valor envuelto a un valor envuelto y devuelve un valor envuelto.

Cuando usamos Promise.then(func) , estamos pasando la Promesa (es decir, C [A]), una función que normalmente tiene la firma A => B y devuelve otra Promesa (es decir, C [B]). Entonces pensé que una Promesa solo sería un Functor y no una Mónada ya que la func devuelve B y no C [B].

Sin embargo, buscando en Google descubrí que una Promesa no es solo un Functor, sino también una Mónada. Me pregunto por qué, ya que func no devuelve un valor envuelto C [B] sino solo B. ¿Qué me estoy perdiendo?


Las promesas no son mónadas sobre objetos que contienen una propiedad Then

Las promesas tratan los objetos que contienen una propiedad then que es una función como un caso especial. Debido a esto, violan la ley de identidad de la izquierda de la siguiente manera:

//Law of left identity is violated // g(v) vs Promise.resolve(v).then(g) // identity function saved under `then` prop const v = ({then: x=>x({then: 1})}) // `g` returns the `then` prop from object wrapped in a promise const g = (obj => Promise.resolve(obj.then)) g(v).then(res => console.log("g(v) returns", res)) // "g(v) returns" x => x({ then: 1 }) Promise.resolve(v).then(g) .then(res => console.log("Promise.resolve(v).then(g) returns", res)) // "Promise.resolve(v).then(g) returns" 1

ejemplo en codepen

Esto sucede porque la resolución trata la función bajo la propiedad then como una devolución de llamada, pasando la continuación de la cadena entonces como argumento en lugar de crear una promesa que lo contenga. De esta manera, no funciona como unidad y causa una violación de las leyes de mónada.

Sin embargo, sobre valores que no contienen una propiedad then, debería funcionar como mónada.


FECHA Vea esta nueva biblioteca que prueba el operador de fundas y operadores de mónada para funciones simples basadas en devolución de llamada que no tienen los problemas con los recursos que se detallan a continuación:

https://github.com/dmitriz/cpsfy

JS Promise no es ni Functor ni Aplicativo ni Mónada.

No es un functor, porque se viola la ley de preservación de la composición (envío de composiciones de funciones a composiciones de sus imágenes):

promise.then(x => g(f(x)))

NO es equivalente a

promise.then(f).then(g)

Lo que esto significa en términos prácticos, nunca es seguro refactorizar

promise .then(x => f(x)) .then(y => g(y))

a

promise .then(x => g(f(x))

como hubiera sido, si Promise functor.

Comprobante de la violación de la ley de functor. Aquí hay un contraejemplo:

//Functor composition preservation law: // promise.then(f).then(g) vs promise.then(x => g(f(x))) // f takes function `x` // and saves it in object under `then` prop: const f = x => ({then: x}) // g returns the `then` prop from object const g = obj => obj.then // h = compose(g, f) is the identity const h = x => g(f(x)) // fulfill promise with the identity function const promise = Promise.resolve(a => a) // this promise is fulfilled with the identity function promise.then(h) .then(res => { console.log("then(h) returns: ", res) }) // => "then(h) returns: " a => a // but this promise is never fulfilled promise.then(f) .then(g) .then(res => { console.log("then(f).then(g) returns: ", res) }) // => ??? // because this one isn''t: promise.then(f) .then(res => { console.log("then(f) returns: ", res) })

Aquí está este ejemplo en Codepen: https://codepen.io/dmitriz/pen/QrMawp?editors=0011

Explicación

Como la composición h es la función de identidad, promise.then(h) simplemente adopta el estado de promise , que ya se cumple con la identidad a => a .

Por otro lado, f devuelve el llamado thenable :

1.2. "Thenable" es un objeto o función que define un método then.

Para mantener la ley de functor, .then tendría que simplemente envolver en promesa el resultado f(x) . En cambio, la especificación de promesa requiere un comportamiento diferente cuando la función dentro de .then devuelve un "thenable". Según 2.3.3.3 , la función de identidad id = a => a almacenada bajo la clave se llama como

id(resolvePromise, rejectPromise)

donde resolvePromise y rejectPromise son dos funciones de devolución de llamada proporcionadas por el procedimiento de resolución de promesas. Pero luego, para ser resuelto o rechazado, se debe llamar a una de estas funciones de devolución de llamada, ¡lo cual nunca sucede! Entonces la promesa resultante permanece en el estado pendiente.

Conclusión

En este ejemplo, promise.then(x => g(f(x))) se cumple con la función de identidad a => a , mientras que promise.then(f).then(g) permanece en el estado pendiente para siempre. Por lo tanto, estas dos promesas no son equivalentes y, por lo tanto, se viola la ley de los fundadores.

Promise no es una Monad ni un Applicative

Porque incluso la ley de transformación natural de la Especificación de Funcionador en punta , que es parte de ser Applicative (la ley de homomorfismo), se viola:

Promise.resolve(g(x)) is NOT equivalent to Promise.resolve(x).then(g)

Prueba. Aquí hay un contraejemplo:

// identity function saved under `then` prop const v = ({then: a => a}) // `g` returns `then` prop from object const g = obj => obj.then // `g(v)` is the identity function Promise.resolve(g(v)).then(res => { console.log("resolve(g(v)) returns: ", res) }) // => "resolve(g(v)) returns: " a => a // `v` is unwrapped into promise that remains pending forever // as it never calls any of the callbacks Promise.resolve(v).then(g).then(res => { console.log("resolve(v).then(g) returns: ", res) }) // => ???

Este ejemplo en Codepen: https://codepen.io/dmitriz/pen/wjqyjY?editors=0011

Conclusión

En este ejemplo, una vez más se cumple una promesa, mientras que la otra está pendiente, por lo tanto, las dos no son equivalentes en ningún sentido, violando la ley.

ACTUALIZAR.

¿Qué significa exactamente "ser Functor"?

Parece haber una confusión entre la promesa de ser un Functor / Aplicativo / Mónada como es, y las formas de hacerlo cambiando sus métodos o agregando otros nuevos. Sin embargo, un Functor debe tener un método de map (no necesariamente bajo este nombre) ya proporcionado, y ser Functor depende claramente de la elección de este método. El nombre real del método no juega ningún papel, siempre que se cumplan las leyes.

Para las promesas, .then es la opción más natural, que falla la ley de Functor como se explica a continuación. Ninguno de los otros métodos de Promise lo convertiría en Functor de ninguna manera concebible, por lo que puedo ver.

Cambiar o agregar métodos

Es diferente si se pueden definir otros métodos que se ajusten a las leyes. La única implementación en esta dirección que conozco es proporcionada por la biblioteca de credos .

Pero hay que pagar un precio considerable : no solo se debe definir un método de map completamente nuevo, sino que también se deben cambiar los objetos de la promesa en sí: una promesa de creed puede tener un valor "accesible", mientras que la promesa nativa de JS puede " t. Este cambio es sustancial y necesario para evitar infringir la ley en los ejemplos como se explica a continuación. En particular, no conozco ninguna forma de convertir la promesa en un Functor (o una mónada) sin esos cambios fundamentales.


Según yo, las Promesas son Functores, Funcionarios Aplicativos y Mónadas, ya que obedecen las leyes de fundadoras y mónadas.

Ok, estudiemos el caso del functor. Para que Promises sea una instancia de Functor, debemos definir una función fmap (a -> b) - fa -> fb para Promises y fmap debe aprobar las leyes de Functor. ¿Qué son las leyes de functor?

fmap id = id fmap (p . q) = (fmap p) . (fmap q)

  • id es la función de identidad. Simplemente podemos implementarlo en JS como var id = x => x
  • El . en (p . q) es la operación de composición al igual que en matemáticas. Es esencialmente var dot = p => q => x => p(q(x)) en JS.

El problema en JS es que los objetos, incluidas las funciones, son tipos de referencia, lo que significa que, a diferencia de Haskell, cada vez que aplicas parcialmente una función, obtendrás una función diferente haciendo lo mismo. Entonces, solo las verificaciones de equidad en las siguientes leyes fallarán, pero pasarán si verifica los valores resultantes.

var id = x => x, dot = f => g => x => f(g(x)), fmap = f => p => p.then(v => f(v)), pr1 = Promise.resolve(1); fmap(id)(pr1) === id(pr1); // false since objects are mutable fmap(id)(pr1).then(v => console.log(v)); id(pr1).then(v=> console.log(v)); fmap(dot(x => x*2)(y => y+5))(pr1).then(v => console.log(v)); dot(fmap(x => x*2))(fmap(y => y+5))(pr1).then(v => console.log(v));

Entonces, sí, las promesas son functores y si revisas las leyes de la mónada , puedes decir fácilmente que también son mónadas.


Una promesa puede ser de hecho una Mónada, tal vez no con la implementación nativa estándar, porque no hay una definición explícita de un método Bind en el objeto Promise. La mayoría de la gente parece entender mal el concepto y equiparar el método con el vínculo monádico . si ampliamos Promise con:

Promise.prototype.bind = function(func) { var initialPromise = this; return new Promise(function(resolve) { initialPromise.then(result => func(result).then(x => resolve(x))) }); }

podemos verificar las 3 leyes monádicas:

var id = x => new Promise((resolve) => resolve(x)); // Law 1 -Left identity: return a >>= f ≡ f a var value = 1 var f = x => id(x * 2); //id(value).bind(f) ==f(value) id(value).bind(f).then(console.log) f(value) .then(console.log) // Law 2 -Right Identity : m >>= return ≡ m var m = id(1) //m.bind(id) == m m.bind(id).then(console.log) m .then(console.log) //Law 3 -Associativity: (m flatMap f) flatMap g assert_=== m flatMap { x => f(x) flatMap {g} } var m = id(1); var f = x => id(x * 2); var g = x => id(x * 5); m.bind(f).bind(g) .then(console.log); m.bind(x=>f(x).bind(g)) .then(console.log);

para una discusión más extensa consulte este artículo Redescubriendo promesas en Javascript

y jugar con el violín aquí - Promesa como mónada: leyes monádicas


Promise es (muy parecido) una mónada porque then está sobrecargada.

Cuando usamos Promise.then (func), estamos pasando la Promesa (es decir, C [A]), una función que normalmente tiene la firma A => B y devuelve otra Promesa (es decir, C [B]). Entonces pensé que una Promesa solo sería un Functor y no una Mónada ya que la función devuelve B y no C [B].

esto es cierto para then(Promise<A>, Func<A, B>) : Promise<B> (si disculpa mi pseudocódigo para los tipos de JavaScript, describiré las funciones como si this fuera el primer argumento)

Sin embargo, la API de Promise proporciona otra firma para then , then(Promise<A>, Func<A, Promise<B>>) : Promise<B> . Esta versión obviamente se ajusta a la firma para el enlace monádico ( >>= ). Pruébalo tú mismo, funciona.

sin embargo, ajustar la firma de una mónada no significa que Promise sea una mónada. También necesita satisfacer las leyes algebraicas para las mónadas.

las leyes que debe cumplir una mónada son la ley de asociatividad

(m >>= f) >>= g ≡ m >>= ( /x -> (f x >>= g) )

y las leyes de identidad izquierda y derecha

(return v) >>= f ≡ f v m >>= return ≡ m

en JavaScript:

function assertEquivalent(px, py) { Promise.all([px, py]).then(([x, y]) => console.log(x === y)); } var _return = x => Promise.resolve(x) Promise.prototype.bind = Promise.prototype.then var p = _return("foo") var f = x => _return("bar") var g = y => _return("baz") assertEquivalent( p.bind(f).bind(g), p.bind(x => f(x).bind(g)) ); assertEquivalent( _return("foo").bind(f), f("foo") ); assertEquivalent( p.bind(x => _return(x)), p );

Creo que cualquiera que esté familiarizado con las promesas puede ver que todo esto debería ser cierto, pero no dude en probarlo usted mismo.

Debido a que Promise es una mónada, también podemos derivar ap y obtener un aplicativo, dándonos una sintaxis muy agradable con un poco de piratería informática mal aconsejada:

Promise.prototype.ap = function (px) { return this.then(f => px.then(x => f(x))); } Promise.prototype.fmap = function(f) { return this.then(x => f(x)); } // to make things pretty and idiomatic Function.prototype.doFmap = function(mx) { return mx.fmap(this); } var h = x => y => x + y // (h <$> return "hello" <*> return "world") >>= printLn h.doFmap(_return("hello, ")).ap(_return("world!")).bind(console.log)