是否继续传递风格与管道有什么不同?

时间:2012-04-16 12:12:19

标签: javascript pipe continuation-passing

我一直在学习continuation passing style,尤其是在javascript中实现的asynchronous version,其中一个函数将另一个函数作为最终参数并创建一个异步调用,将返回值传递给第二个功能。

但是,我不能完全看到continuation-passing做什么比重新创建管道(如在unix命令行管道中)或流:

replace('somestring','somepattern', filter(str, console.log));

vs

echo 'somestring' | replace 'somepattern' | filter | console.log

除了管道更多,更清洁。使用管道,很明显数据被传递,同时执行被传递给接收程序。事实上,对于管道,我希望数据流能够继续向下传递管道,而在CPS中我期望一个串行过程。

如果将通信对象和更新方法与数据一起传递,而不是完整的切换和返回,可能可以想象CPS可以扩展到连续管道。

我错过了什么吗? CPS在某些重要方面有所不同(更好吗?)?

要清楚,我的意思是继续传递,其中一个函数将执行传递给另一个函数,而不仅仅是普通的回调。 CPS似乎暗示将函数的返回值传递给另一个函数,然后退出。

2 个答案:

答案 0 :(得分:8)

UNIX管道与异步javascript

unix管道的行为方式与链接到的异步CPS代码之间有很大的根本区别。

主要是管道阻塞执行,直到整个链完成,而您的异步CPS示例将在进行第一个异步调用后立即返回,并且仅在完成回调后才执行。 (在您的示例中,超时等待完成后。)

看看这个例子。我将使用Fetch API和Promises来演示异步行为,而不是setTimeout来使它更​​加逼真。想象一下,第一个函数f1()负责调用某些Web服务并将结果解析为json。这是“管道”到处理结果的f2()中。

CPS样式

function f2(json){
    //do some parsing
}

function f1(param, next) {
   return fetch(param).then(response => response.json()).then(json => next(json));
}

// you call it like this:
f1("https://service.url", f2);

如果将对f2的调用从f1中移出,则可以编写语法上看起来像管道的东西,但这与上面的操作完全相同:

function f1(param) {
   return fetch(param).then(response => response.json());
}

// you call it like this:
f1("https://service.url").then(f2);

但这仍然不会阻止。您无法使用javascript中的阻止机制来执行此任务,根本就没有在Promise上阻止的机制。 (在这种情况下,您可以使用同步XMLHttpRequest,但这不是重点。)

CPS与管道系统

以上两种方法之间的区别在于,谁可以控制决定是否调用下一步,以及确切地使用参数,调用方(后例)或被调用函数(CPS)来决定

使用CPS很好用的一个很好的例子是中间件。考虑例如在处理管道中的缓存中间件。简化示例:

function cachingMiddleware(request, next){
     if(someCache.containsKey(request.url)){
         return someCache[request.url];
     }
     return next(request);
}

中间件执行一些逻辑,检查缓存是否仍然有效:

  • 如果不是,则调用next,然后将继续进行处理管道。

  • 如果有效,则返回缓存的值,跳过下一次执行。

答案 1 :(得分:4)

应用程序级别的继续传递样式

在应用程序级别考虑连续传递样式,而不是在表达式/功能块级别进行比较,可以通过其“继续”函数(又称为回调函数)来提供流控制优势的途径。让我们以Express.js为例:

每个express middleware都有一个非常相似的CPS功能签名:

 const middleware = (req, res, next) => {
     /* middleware's logic */
     next();
 }

 const customErrorHandler = (error, req, res, next) => {
     /* custom error handling logic*/
 };

next是express的本机回调函数。

  

更正:next()函数不是Node.js或Express API的一部分,而是传递给中间件函数的第三个参数。 next()函数可以命名为任何名称,但按照惯例,它始终命名为“ next”

reqres分别是HTTP请求和HTTP响应的命名约定。

Express.JS中的路由处理程序将由一个或多个中间件功能组成。 Express.js会将前一个中间件所做的更改传递给reqres对象,并将它们传递给下一个对象,并传递相同的next回调。

app.get('/get', middlware1, middlware2, /*...*/ , middlewareN, customErrorHandler)

next回调函数用于:

  1. 作为中间件的 continuation

    • 调用next()将执行流传递到下一个中​​间件功能。在这种情况下,它可以发挥延续的作用。
  2. 也用作路由拦截器

    • 调用next('Custom error message')绕过所有后续中间件,并将执行控制传递给customErrorHandler进行错误处理。这样可以在路线中间进行“取消”!
    • 调用next('route')绕过后续的中间件,并将控制权传递给下一个匹配的路由,例如。 / get / part。

JS中的模仿管道

有一个pipe的TC39建议,但在被接受之前,我们将不得不手动模仿管道的行为。嵌套CPS函数可能会导致回调地狱,所以这是我尝试使用的更简洁的代码:

假设我们要计算一个句子“狐狸跳过月球” ,方法是替换一部分启动字符串(例如props

const props = "     The [ANIMAL] [ACTION] over the [OBJECT] "

每个替换字符串不同部分的函数都按数组排序

const insertFox = s => s.replace(/\[ANIMAL\]/g, 'fox')
const insertJump = s => s.replace(/\[ACTION\]/g, 'jumps')
const insertMoon = s => s.replace(/\[OBJECT\]/g, 'moon')
const trim = s => s.trim()
const modifiers = [insertFox, insertJump, insertMoon, trim]

我们可以使用reduce实现同步的,非流式的管道行为。

const pipeJS = (chain, callBack) => seed => 
    callBack(chain.reduce((acc, next) => next(acc), seed))
const callback = o => console.log(o)

pipeJS(modifiers, callback)(props) //-> 'The fox jumps over the moon'

这是pipeJS的异步版本;

const pipeJSAsync = chain => async seed =>
    await chain.reduce((acc, next) => next(acc), seed)
const callbackAsync = o => console.log(o)

pipeJSAsync(modifiers)(props).then(callbackAsync) //-> 'The fox jumps over the moon'

希望这会有所帮助!