¿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.