javascript functional-programming pipe monads

¿Cómo funcionan juntos las tuberías y las mónadas en JavaScript?



functional-programming pipe (2)

He examinado preguntas y respuestas similares y no he encontrado una respuesta que responda directamente a mi pregunta. Me cuesta entender cómo usar Maybe o Either o Monads junto con las funciones de canalización. Quiero canalizar las funciones juntas, pero quiero que la tubería se detenga y devuelva un error si se produce uno en cualquier paso. Estoy tratando de implementar conceptos de programación funcional en una aplicación node.js, y esta es realmente mi primera exploración seria de ambos, por lo que ninguna respuesta será tan simple como insultar mi inteligencia sobre el tema.

He escrito una función de tubería como esta:

const _pipe = (f, g) => async (...args) => await g( await f(...args)) module.exports = {arguments. pipeAsync: async (...fns) => { return await fns.reduce(_pipe) }, ...

Lo llamo así:

const token = await utils.pipeAsync(makeACall, parseAuthenticatedUser, syncUserWithCore, managejwt.maketoken)(x, y)


La respuesta de Naomik es muy interesante, pero no parece que realmente haya respondido tu pregunta.

La respuesta corta es que su función _pipe propaga los errores perfectamente. Y deja de ejecutar funciones tan pronto como uno arroja un error.

El problema está en su función pipeAsync , donde tuvo la idea correcta, pero innecesariamente le devuelve una promesa de una función en lugar de una función.

Es por eso que no puedes hacer esto, porque arroja un error cada vez:

const result = await pipeAsync(func1, func2)(a, b);

Para usar pipeAsync en su estado actual, necesitaría dos await : una para obtener el resultado de pipeAsync y otra para obtener el resultado de llamar a ese resultado:

const result = await (await pipeAsync(func1, func2))(a, b);

La solución

Elimine el async innecesario y await de la definición de pipeAsync . El acto de componer una serie de funciones, incluso funciones asincrónicas, no es una operación asincrónica:

module.exports = { pipeAsync: (...fns) => fns.reduce(_pipe),

Una vez que hayas hecho eso, todo funciona bien:

const _pipe = (f, g) => async(...args) => await g(await f(...args)) const pipeAsync = (...fns) => fns.reduce(_pipe); const makeACall = async(a, b) => a + b; const parseAuthenticatedUser = async(x) => x * 2; const syncUserWithCore = async(x) => { throw new Error("NOOOOOO!!!!"); }; const makeToken = async(x) => x - 3; (async() => { const x = 9; const y = 7; try { // works up to parseAuthenticatedUser and completes successfully const token1 = await pipeAsync( makeACall, parseAuthenticatedUser )(x, y); console.log(token1); // throws at syncUserWithCore const token2 = await pipeAsync( makeACall, parseAuthenticatedUser, syncUserWithCore, makeToken )(x, y); console.log(token2); } catch (e) { console.error(e); } })();

Esto también se puede escribir sin usar async :

const _pipe = (f, g) => (...args) => Promise.resolve().then(() => f(...args)).then(g); const pipeAsync = (...fns) => fns.reduce(_pipe); const makeACall = (a, b) => Promise.resolve(a + b); const parseAuthenticatedUser = (x) => Promise.resolve(x * 2); const syncUserWithCore = (x) => { throw new Error("NOOOOOO!!!!"); }; const makeToken = (x) => Promise.resolve(x - 3); const x = 9; const y = 7; // works up to parseAuthenticatedUser and completes successfully pipeAsync( makeACall, parseAuthenticatedUser )(x, y).then(r => console.log(r), e => console.error(e)); // throws at syncUserWithCore pipeAsync( makeACall, parseAuthenticatedUser, syncUserWithCore, makeToken )(x, y).then(r => console.log(r), e => console.error(e))


anzuelo, línea y plomo

No puedo enfatizar lo importante que es que no te enredes en todos los términos nuevos que sientes que tienes que aprender: la programación funcional se trata de funciones , y quizás lo único que debes entender sobre la función es que le permite abstraer parte de su programa usando un parámetro; o múltiples parámetros si es necesario (no lo es) y es compatible con su idioma (generalmente lo es)

¿Por qué te estoy diciendo esto? Bueno, JavaScript ya tiene una API perfectamente buena para secuenciar funciones asincrónicas utilizando el Promise.prototype.then

// never reinvent the wheel const _pipe = (f, g) => async (...args) => await g( await f(...args)) myPromise .then (f) .then (g) .then (h) ...

Pero quieres escribir programas funcionales, ¿verdad? Esto no es problema para el programador funcional. Aísle el comportamiento que desea abstraer (ocultar) y simplemente envuélvalo en una función parametrizada: ahora que tiene una función, reanude la escritura de su programa en un estilo funcional ...

Después de hacer esto por un tiempo, comienza a notar patrones de abstracción: estos patrones servirán como casos de uso para todas las demás cosas (functores, solicitantes, mónadas, etc.) de las que aprenderá más adelante, pero las guardará para más adelante . ahora, funciones ...

A continuación, demostramos la composición de izquierda a derecha de funciones asincrónicas a través de comp . Para los fines de este programa, el delay se incluye como creador de Promises, y sq y add1 son funciones asincrónicas de muestra:

const delay = (ms, x) => new Promise (r => setTimeout (r, ms, x)) const sq = async x => delay (1000, x * x) const add1 = async x => delay (1000, x + 1) // just make a function const comp = (f, g) => // abstract away the sickness x => f (x) .then (g) // resume functional programming const main = comp (sq, add1) // print promise to console for demo const demo = p => p .then (console.log, console.error) demo (main (10)) // 2 seconds later... // 101

inventa tu propia conveniencia

Puede hacer una compose variada que acepte cualquier número de funciones (también observe cómo esto le permite mezclar funciones de sincronización y asíncrona en la misma composición), un beneficio de conectarse directamente a .then , que automáticamente promueve valores de retorno que no son Promesa a una Promesa -

const delay = (ms, x) => new Promise (r => setTimeout (r, ms, x)) const sq = async x => delay (1000, x * x) const add1 = async x => delay (1000, x + 1) // make all sorts of functions const effect = f => x => ( f (x), x ) // invent your own convenience const log = effect (console.log) const comp = (f, g) => x => f (x) .then (g) const compose = (...fs) => fs .reduce (comp, x => Promise .resolve (x)) // your ritual is complete const main = compose (log, add1, log, sq, log, add1, log, sq) // print promise to console for demo const demo = p => p .then (console.log, console.error) demo (main (10)) // 10 // 1 second later ... // 11 // 1 second later ... // 121 // 1 second later ... // 122 // 1 second later ... // 14884

trabaja inteligentemente y no duro

comp y compose son funciones fáciles de digerir que casi no requirieron esfuerzo para escribir. Debido a que utilizamos .then , todo el manejo de errores se engancha automáticamente. No tiene que preocuparse por await manualmente o try/catch o .catch , otro beneficio más de escribir nuestras funciones de esta manera:

sin vergüenza en la abstracción

Ahora, eso no quiere decir que cada vez que escriba una abstracción sea con el propósito de ocultar algo malo , pero puede ser muy útil para una variedad de tareas, por ejemplo, "ocultar" el estilo imperativo while :

const fibseq = n => // a counter, n { let seq = [] // the sequence we will generate let a = 0 // the first value in the sequence let b = 1 // the second value in the sequence while (n > 0) // when the counter is above zero { n = n - 1 // decrement the counter seq = [ ...seq, a ] // update the sequence a = a + b // update the first value b = a - b // update the second value } return seq // return the final sequence } console .time (''while'') console .log (fibseq (500)) console .timeEnd (''while'') // [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... ] // while: 3ms

Pero quieres escribir programas funcionales, ¿verdad? Esto no es problema para el programador funcional. Podemos hacer nuestro propio mecanismo de bucle, pero esta vez usará funciones y expresiones en lugar de declaraciones y efectos secundarios, todo sin sacrificar la velocidad, la legibilidad o la seguridad de la pila .

Aquí, loop aplica continuamente una función utilizando nuestro contenedor de valores recur . Cuando la función devuelve un valor no recur , se completa el cálculo y se devuelve el valor final. fibseq es una expresión pura y funcional completa con recursión ilimitada. Ambos programas calculan el resultado en aproximadamente 3 milisegundos. No olvides comprobar que las respuestas coinciden: D

const recur = (...values) => ({ recur, values }) // break the rules sometimes; reinvent a better wheel const loop = f => { let acc = f () while (acc && acc.recur === recur) acc = f (...acc.values) return acc } const fibseq = x => loop // start a loop with vars ( ( n = x // a counter, n, starting at x , seq = [] // seq, the sequence we will generate , a = 0 // first value of the sequence , b = 1 // second value of the sequence ) => n === 0 // once our counter reaches zero ? seq // return the sequence : recur // otherwise recur with updated vars ( n - 1 // the new counter , [ ...seq, a ] // the new sequence , b // the new first value , a + b // the new second value ) ) console.time (''loop/recur'') console.log (fibseq (500)) console.timeEnd (''loop/recur'') // [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... ] // loop/recur: 3ms

nada es sagrado

Y recuerda, puedes hacer lo que quieras. then no hay nada mágico: alguien, en algún lugar, decidió hacerlo. Podrías ser alguien en algún lugar y simplemente hacer el tuyo then , aquí hay una especie de función de composición hacia adelante, al igual que Promise.prototype.then , se aplica automáticamente a valores de retorno Promise.prototype.then que se Promise.prototype.then ; agregamos esto no porque sea una idea particularmente buena, sino para mostrar que podemos hacer ese tipo de comportamiento si quisiéramos.

const then = x => x && x.then === then ? x : Object .assign ( f => then (f (x)) , { then } ) const sq = x => then (x * x) const add1 = x => x + 1 const effect = f => x => ( f (x), x ) const log = effect (console.log) then (10) (log) (sq) (log) (add1) (add1) (add1) (log) // 10 // 100 // 101 sq (2) (sq) (sq) (sq) (log) // 65536

¿qué idioma es ese?

Ya ni siquiera parece JavaScript, pero ¿a quién le importa? Es su programa y usted decide cómo quiere que se vea. Un buen lenguaje no se interpondrá en su camino y lo obligará a escribir su programa en un estilo particular; funcional o de otra manera.

En realidad, es JavaScript, simplemente desinhibido por conceptos erróneos de lo que es capaz de expresar:

const $ = x => k => $ (k (x)) const add = x => y => x + y const mult = x => y => x * y $ (1) // 1 (add (2)) // + 2 = 3 (mult (6)) // * 6 = 18 (console.log) // 18 $ (7) // 7 (add (1)) // + 1 = 8 (mult (8)) // * 8 = 64 (mult (2)) // * 2 = 128 (mult (2)) // * 2 = 256 (console.log) // 256

Cuando entiendas $ , habrás entendido a la madre de todas las mónadas . Recuerde concentrarse en la mecánica y obtener una intuición de cómo funciona ; Preocúpese menos por los términos.

envíalo

Acabamos de usar los nombres comp y compose en nuestros fragmentos locales, pero cuando empaqueta su programa, debe elegir nombres que tengan sentido dado su contexto específico; vea el comentario de Bergi para una recomendación.