在JavaScript中,管道和单子如何一起工作?

14

我看过类似的问题和答案,但没有找到直接回答我的问题的答案。我不知道如何在使用函数管道时结合 MaybeEither 或者 Monads。我希望将函数连接起来,但如果任何一步出现错误,管道就会停止并返回一个错误。我正在尝试在 node.js 应用程序中实现函数式编程概念,这是我第一次认真探索这个领域,因此没有一个简单的答案可以侮辱我的智商。

我已经写了一个像这样的管道函数:

const _pipe = (f, g) => async (...args) => await g( await f(...args))

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

我这样调用它:

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

1
{btsdaf} - Mulan
{btsdaf} - Bergi
2
第三行的 arguments. 是一个错别字,还是我从未见过的语法? - JLRishe
2个回答

29

钓鱼竿,线和沉重的铅坠

我强调你不要被所有新术语所困扰——函数式编程是关于“函数”的。也许你只需要了解函数允许你使用参数来抽象你程序的一部分;如果需要(一般不需要),还可以使用多个参数,这取决于你的编程语言是否支持。

我为什么要告诉你这些?因为JavaScript已经有了一个完美的API来顺序执行异步函数,那就是内置的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) ...

但你希望编写函数式程序,对吧?这对函数式程序员来说不是问题。将你想要抽象(隐藏)的行为隔离出来,并简单地将其包装在一个参数化的函数中 - 现在你有了一个函数,可以继续以函数式风格编写程序...

这样做一段时间后,你会开始注意到抽象的模式 - 这些模式将成为你以后要学习的所有其他东西(函子、应用函子、单子等)的使用案例 - 但现在先保留这些内容,专注于函数 ...

下面,我们通过comp演示了异步函数的从左到右组合。为了这个程序的目的,delay作为Promise创建器被包含在其中,而sqadd1则是示例异步函数 -

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

创造自己的便利

您可以创建一个可变参数的compose,它可以接受任意数量的函数 - 还请注意,这允许您在同一组合中混合同步和异步函数 - 这是直接连接到.then的好处,它会自动将非Promise返回值提升为Promise。

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

更聪明地工作,而不是更辛苦

compcompose 是易于理解的函数,几乎不需要任何努力就可以编写。因为我们使用了内置的.then,所有错误处理都会自动钩连起来。您无需担心手动awaittry/catch.catch - 这是我们以这种方式编写函数的又一个好处。

抽象没有什么可耻的

这并不意味着每次编写抽象时都是为了隐藏一些不好的东西,但它可以非常有用地完成各种任务 - 例如“隐藏”命令式风格的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

但是你想编写函数式程序,对吧?对于函数式程序员来说这不是问题。我们可以制作自己的循环机制,但这次它将使用函数和表达式而不是语句和副作用-所有这些都不会牺牲速度、可读性或堆栈安全性

在这里,loop 不断地使用我们的 recur 值容器应用函数。当函数返回一个非 recur 值时,计算完成,最终值被返回。fibseq 是一个纯粹的、具有无限递归的函数式表达式。这两个程序都可以在大约3毫秒内计算出结果。别忘了检查答案是否匹配: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

没有什么是神圣的

记住,你可以做任何想做的事情。 then 没有什么神奇的地方 - 在某个地方,某人决定将其制作。 你可以成为某个地方的某个人,并且只需制作自己的 then - 这里的 then 是一种前向组合函数 - 就像 Promise.prototype.then 一样,它会自动将 then 应用于非 then 返回值; 我们添加这个不是因为这是一个特别好的想法,而是为了表明如果我们想要的话,我们可以制作出那种行为。

const then = 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

这是什么语言?

看起来甚至不像JavaScript了,但是谁在意呢?这是你的程序,可以决定它的样子。好的语言不会阻止你按照任何特定风格编写程序,无论是函数式还是其他风格。

实际上,它就是JavaScript,只是没有限制它所能表达的误解 -

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

当你理解了$,你就理解了所有monad之母。记得关注它的机制并对它是如何工作的有直觉;不要过于担心术语。

发布它

我们刚刚在本地片段中使用了compcompose这两个名称,但是当您打包程序时,应该选择符合您特定上下文的名称-请参见Bergi的评论以获取建议。


1
{btsdaf} - Bergi
1
async/await is Promise.prototype.then behind the scenes - it's just a different syntax – we *could' use await but the point is to show that such heavy machinery is not necessary; it's overkill when we get the exact behavior we want with a simple f(x).then(g) - Mulan
1
{btsdaf} - Mulan
我不禁一直在思考你的 then 作为一种前向组合。我想知道我们是否可以提供最后一个值:comp = n => f => x => n === 1 ? f.reduce((acc, g) => g(acc), x) : comp(n - 1) ([x].concat(f));。应用如下:comp(4) (inc) (inc) (inc) (sqr) (2)。部分应用:comp4 = comp(4)。也许你能想出一个不需要 n 的解决方案?!? - user6445533
const comp = f => Object.assign(g => comp([g].concat(f)), {run: x => f.reduce((acc, h) => h(acc), x)}); 太棒了。 - user6445533
显示剩余9条评论

4
naomik的回答非常有趣,但似乎她并没有真正回答你的问题。
简短的答案是,你的_pipe函数可以很好地传播错误。并且一旦函数抛出错误,就会停止运行函数。
问题在于你的pipeAsync函数,你的想法是正确的,但你没必要让它返回一个函数的promise,而不是一个函数。
这就是为什么你不能这样做,因为它每次都会抛出一个错误。
const result = await pipeAsync(func1, func2)(a, b);

为了使用pipeAsync,您需要两个await:一个用于获取pipeAsync的结果,另一个用于获取调用该结果的结果。
const result = await (await pipeAsync(func1, func2))(a, b);
解决方案pipeAsync的定义中删除不必要的asyncawait。即使是异步函数,组合一系列函数的操作也不是异步操作:
module.exports = {
    pipeAsync: (...fns) => fns.reduce(_pipe),

一旦您完成这些步骤,一切都可以正常运作:

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);
  }
})();

这也可以完全不使用 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))


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接