功能编程和异步/承诺

时间:2018-02-09 18:11:02

标签: javascript node.js asynchronous functional-programming ramda.js

我将一些旧节点模块重构为更实用的样式。对于FP来说,我就像第二年新生一样。我不停地挂起来处理大型异步流程。下面是一个示例,我向db发出请求,然后缓存响应:



// Some external xhr/promise lib
const fetchFromDb = make => {
  return new Promise(resolve => {
    console.log('Simulate async db request...'); // just simulating a async request/response here.
    setTimeout(() => {
      console.log('Simulate db response...');
      resolve({ make: 'toyota', data: 'stuff' }); 
    }, 100);
  });
};

// memoized fn
// this caches the response to getCarData(x) so that whenever it is invoked with 'x' again, the same response gets returned.
const getCarData = R.memoizeWith(R.identity, (carMake, response) => response.data);

// Is this function pure? Or is it setting something outside the scope (i.e., getCarData)?
const getCarDataFromDb = (carMake) => {
  return fetchFromDb(carMake).then(getCarData.bind(null, carMake));
  // Note: This return statement is essentially the same as: 
  // return fetchFromDb(carMake).then(result => getCarData(carMake, result));
};

// Initialize the request for 'toyota' data
const toyota = getCarDataFromDb('toyota'); // must be called no matter what

// Approach #1 - Just rely on thenable
console.log(`Value of toyota is: ${toyota.toString()}`);
toyota.then(d => console.log(`Value in thenable: ${d}`)); // -> Value in thenable: stuff

// Approach #2 - Just make sure you do not call this fn before db response.
setTimeout(() => { 
  const car = getCarData('toyota'); // so nice!
  console.log(`later, car is: ${car}`); // -> 'later, car is: stuff'
}, 200);

<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
&#13;
&#13;
&#13;

我非常喜欢 memoization 来缓存大型JSON对象和其他计算属性。但是由于很多异步请求的响应依赖于彼此的工作,我无法跟踪我拥有的信息和时间。我想远离使用promises来管理流量。它是一个节点应用程序,所以使事情同步以确保可用性阻止事件循环并真正影响性能。

我更喜欢方法#2 ,我可以使用getCarData('toyota')来获取汽车数据。但缺点是我必须确保已经返回了响应。使用方法#1 ,我总是必须使用一个可以缓解#2方法问题的但是引入了自己的问题。

问题

  1. getCarFromDb是一个纯函数,如上所述?如果不是,那怎么不是副作用?
  2. 以这种方式使用mem反模式的FP反模式?也就是说,从一个带有响应的thenable调用它,以便将来调用同一个方法返回缓存的值?

2 个答案:

答案 0 :(得分:6)

问题1

这里几乎是一个哲学问题,这里是否存在副作用。调用它会更新memoization缓存。但这本身没有可观察到的副作用。所以我会说这实际上是纯粹的。

更新:评论指出,由于这会调用IO,因此它永远不会是纯粹的。那是正确的。但这就是这种行为的本质。它作为一个纯函数没有意义。我上面的回答只是副作用,而不是纯度。

问题2

我不能代表整个FP社区,但我可以告诉你,Ramda团队(免责声明:我是Ramda作者)更喜欢避免Promise,更喜欢更合法的类型这样的FutureTask。但是你在这里遇到的问题与那些替代Promise s的类型有关。 (关于以下这些问题的更多信息。)

一般

这里有一个中心点:如果您正在进行异步编程,它将扩展到触及它的应用程序的每个位。没有什么可以改变这个基本事实。使用Promise s / Task s / Future有助于避免使用基于回调的代码的一些模板,但它要求您将后置响应/拒绝代码放在{{1 } / then函数。使用map可以帮助您避免使用基于Promise的代码的一些模板,但它要求您将帖子响应/拒绝代码放在async/await函数中。如果有一天我们在async之上添加了其他内容,它可能具有相同的特征。

(虽然我建议您查看async/awaitFuture而不是Task s,但我只会讨论Promises。无论如何都应该适用相同的想法。)< / p>

我的建议

如果您要记住任何内容,请记住生成的Promise

但是,在处理异步时,必须将依赖于异步调用结果的代码放入函数中。我假设您的第二种方法的Promises仅用于演示目的:使用超时等待网络上的DB结果非常容易出错。但即使使用setTimeout,其余代码也会在setTimeout回调中运行。

因此,不要试图将数据分离的情况分开,而是在数据尚未缓存的情况下,而不是在任何地方使用相同的技术:setTimeout。这可能看起来像这样:

&#13;
&#13;
myPromise.then(... my code ... )
&#13;
// getCarData :: String -> Promise AutoInfo
const getCarData = R.memoizeWith(R.identity, make => new Promise(resolve => {
    console.log('Simulate async db request...')
    setTimeout(() => {
      console.log('Simulate db response...')
      resolve({ make: 'toyota', data: 'stuff' }); 
    }, 100)
  })
)

getCarData('toyota').then(carData => {
  console.log('now we can go', carData)
  // any code which depends on carData
})

// later
getCarData('toyota').then(carData => {
  console.log('now it is cached', carData)
})
&#13;
&#13;
&#13;

在这种方法中,只要您需要汽车数据,就可以致电<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>。它只是第一次实际调用服务器。之后,getCarData(make)将从缓存中提供。但是你到处都用相同的结构来处理它。

我只看到一个合理的选择。我无法判断您是否需要在拨打剩余电话之前必须等待数据,这意味着您可以预先获取数据。如果是这种情况,那么还有一种可能性,一种可以让你跳过备忘录:

&#13;
&#13;
Promise
&#13;
// getCarData :: String -> Promise AutoInfo
const getCarData = make => new Promise(resolve => {
  console.log('Simulate async db request...')
  setTimeout(() => {
    console.log('Simulate db response...')
    resolve({ make: 'toyota', data: 'stuff' }); 
  }, 100)
})

const makes = ['toyota', 'ford', 'audi']

Promise.all(makes.map(getCarData)).then(allAutoInfo => {
  const autos = R.zipObj(makes, allAutoInfo)
  console.log('cooking with gas', autos)
  // remainder of app that depends on auto data here
})
&#13;
&#13;
&#13;

但这意味着在获取所有数据之前一切都无法使用。根据各种因素的不同,这可能也可能不适合你。在许多情况下,它甚至都不可能或不可取。但有可能你的是有用的。

关于您的代码的一个技术要点:

<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

有没有理由使用const getCarDataFromDb = (carMake) => { return fetchFromDb(carMake).then(getCarData.bind(null, carMake)); }; 代替getCarData.bind(null, carMake)?这似乎更具可读性。

答案 1 :(得分:2)

  

getCarFromDb是上面写的纯函数吗?

没有。几乎任何使用I / O的东西都是不纯的。数据库中的数据可能会发生变化,请求可能会失败,因此它无法保证它将返回一致的值。

  

以这种方式使用mem反模式FP反模式?也就是说,从一个带有响应的thenable中调用它,以便将来调用同一个方法返回缓存的值?

这绝对是一个不同步的反模式。在方法#2中,您创建了一个竞争条件,如果数据库查询在不到200毫秒内完成,则操作将成功,如果花费的时间超过200毫秒,则操作将失败。你在代码中标记了一行“太好了!”因为您可以同步检索数据。这向我表明,你正在寻找一种绕过异步问题的方法,而不是直接面对它。

您使用bind和“欺骗”memoizeWith来存储您在事后传递的值的方式也看起来非常笨拙和不自然。

可以利用缓存并以更可靠的方式使用异步。

例如:

// Some external xhr/promise lib
const fetchFromDb = make => {
  return new Promise(resolve => {
    console.log('Simulate async db request...')
    setTimeout(() => {
      console.log('Simulate db response...')
      resolve({ make: 'toyota', data: 'stuff' }); 
    }, 2000);
  });
};

const getCarDataFromDb = R.memoizeWith(R.identity, fetchFromDb);

// Initialize the request for 'toyota' data
const toyota = getCarDataFromDb('toyota'); // must be called no matter what

// Finishes after two seconds
toyota.then(d => console.log(`Value in thenable: ${d.data}`));


// Wait for 5 seconds before getting Toyota data again.
// This time, there is no 2-second wait before the data comes back.
setTimeout(() => { 
    console.log('About to get Toyota data again');
    getCarDataFromDb('toyota').then(d => console.log(`Value in thenable: ${d.data}`));
}, 5000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

这里潜在的一个缺陷是,如果请求失败,您将在缓存中遇到被拒绝的承诺。我不确定解决这个问题的最佳方法是什么,但你肯定需要某种方法来使缓存的那部分无效或在某处实现某种重试逻辑。