【问题标题】:Functional Programming and async/promises函数式编程和异步/承诺
【发布时间】:2018-07-20 13:48:31
【问题描述】:

我正在将一些旧节点模块重构为更实用的样式。当谈到 FP 时,我就像是二年级新生 :) 我一直挂断的地方是处理大型异步流。这是我向数据库发出请求然后缓存响应的示例:

// 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>

我真的很喜欢 memoization 用于缓存大型 JSON 对象和其他计算属性。但是对于很多异步请求,它们的响应相互依赖才能完成工作,我无法跟踪我拥有的信息和时间。我想避免大量使用 Promise 来管理流程。它是一个节点应用程序,因此使事情同步以确保可用性会阻塞事件循环并真正影响性能。

我更喜欢方法#2,我可以通过getCarData('toyota') 简单地获取汽车数据。但缺点是我必须确保响应已经返回。对于方法#1,我总是必须使用thenable,它可以缓解方法#2 的问题,但会引入它自己的问题。

问题

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

【问题讨论】:

  • 为什么我们不使用when then函数?
  • getCarFromDb 是上面写的纯函数吗?" 不是。任何访问 I/O 的东西都是不纯的。

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


【解决方案1】:

问题 1

这里是否存在副作用几乎是一个哲学问题。调用它确实会更新记忆缓存。但这本身并没有明显的副作用。所以我会说这实际上是纯粹的。

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

问题 2

我不能代表整个 FP 社区,但我可以告诉你,Ramda 团队(免责声明:我是 Ramda 的作者)更喜欢避免 Promises,更喜欢更合法的类型,例如 Futures或Tasks。但是你在这里遇到的同样的问题也适用于那些替代Promises 的类型。 (下面是关于这些问题的更多信息。)

一般

这里有一个中心点:如果您在进行异步编程,它会传播到应用程序的每一个接触它的部分。你不会做任何事情来改变这个基本事实。使用Promises/Tasks/Futures 有助于避免一些基于回调的代码样板,但它需要您将后响应/拒绝代码放在then/map 函数中。使用 async/await 可以帮助您避免一些基于 Promise 的代码样板,但它需要您将 post 响应/拒绝代码放入 async 函数中。如果有一天我们在 async/await 之上添加其他东西,它可能具有相同的特征。

(虽然我建议您查看 Futures 或 Tasks 而不是 Promises,但下面我将只讨论 Promises。无论如何,同样的想法应该适用。)

我的建议

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

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

因此,与其尝试区分数据已缓存和未缓存的情况,只需在各处使用相同的技术:myPromise.then(... my code ... )。这可能看起来像这样:

// 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)
})
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

在这种方法中,当您需要汽车数据时,您可以致电getCarData(make)。只有第一次它才会真正调用服务器。之后,Promise 从缓存中取出。但是你在任何地方都使用相同的结构来处理它。

我只看到一种合理的选择。我不知道您关于在进行剩余调用之前必须等待数据的讨论是否意味着您可以预取数据。如果是这种情况,那么还有另一种可能性,它也可以让您跳过记忆:

// 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
})
<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) 而不是() => getCarData(carMake)?这似乎更具可读性。

【讨论】:

  • OP 使用getCarData.bind(null, carMake) 的原因是为了让他可以使用make“预烘焙”对getCarData 的调用,然后在事后传递数据,但这可以被替换使用response => getCarData(carMake, response),但这实际上比使用bind 更长、更重复。缓存承诺的一个问题是我不确定如何解决 - 如果其中一个请求失败,您将被缓存中的拒绝承诺卡住。有没有一种优雅的方法来解决这个问题而不会偏离 FP 原则太远?
  • 是的,当然,我在考虑我的代码,make 是唯一的参数。这是有道理的。
  • 我认为如果使用memoizeWith 缓存您的结果,您对失败没有追索权。编写自己的缓存非常容易,它有多次重试并且从不缓存失败。但是memoizeWith 在这里帮不了你。
  • 更有趣的问题是,如果由专门为其设计的类型的值表示的副作用仍然是副作用,或者更确切地说是一流的效果,您可以通过周围并像普通数据一样组成。这部分适用于Promises,甚至更适用于一元Task 类型。
【解决方案2】:

getCarFromDb 是上面写的纯函数吗?

没有。几乎所有使用 I/O 的东西都是不纯的。数据库中的数据可能会更改,请求可能会失败,因此它不能提供任何可靠的保证来保证它会返回一致的值。

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

这绝对是一种异步反模式。在您的方法#2 中,您正在创建一个竞争条件,如果数据库查询在不到 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>

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

【讨论】:

    猜你喜欢
    • 2018-09-07
    • 2022-01-26
    • 2021-01-22
    • 2018-08-26
    • 1970-01-01
    • 2018-07-01
    • 2015-10-09
    • 2019-01-15
    • 2016-06-22
    相关资源
    最近更新 更多