Ramda JS - 将可选参数传递给管道的第二个函数

Ramda JS - Pass optional argument to the second function of a pipe

我正在尝试重构这段代码:

async function getUserDataByUsername(username, cached = true) {
  const usernameRef = firestore
    .collection("usernames")
    .doc(username.toLowerCase());

  const usernameDoc = await usernameRef.get();

  if (!usernameDoc.exists) {
    throw userErrors.userNotFound();
  }

  const { userId } = usernameDoc.data();

  return memoizedGetUserData(userId, cached);
}

为此,我想把它拆分成更小的部分,如下:

function memoizedGetUserData(userId, cached = true) { 
  ... Fetching from LRU or DB ... 
}

async function getUserId(username) {
  const usernameRef = firestore
    .collection("usernames")
    .doc(username.toLowerCase());

  const usernameDoc = await usernameRef.get();

  if (!usernameDoc.exists) {
    throw userErrors.userNotFound();
  }

  const { userId } = usernameDoc.data();

  return userId;
}

async function getUserDataByUsername(username, cached = true) {
  const userId = await getUserId(username);

  return memoizedGetUserData(userId, cached);
}

现在,我想将 Ramda 应用于此模块。我以前从未使用过这个库,但我读到它真的很酷,并且通过它的一些实用程序使代码更容易理解。

我正在尝试的是使用流水线样式重构原始方法,如下:

import R from "ramda";

...

const getUserDataByUsername = R.pipeP(getUserId, memoizedGetUserData);

但是...我如何将第二个可选参数“缓存”传递给我管道的第二个参数?

const R = require("ramda")
function memoizedGetUserData(data) {
 const [userId, cached = true] = data
  console.log(userId,cached); // hello false
}
//
async function getUserId(...data) {
  const [username,cached] = data
  const userId = await Promise.resolve(username)
  return [userId,cached];
}

//  memoizedGetUserData  arguments must be unary
const getUserDataByUsername = R.pipeP(getUserId,memoizedGetUserData)

getUserDataByUsername('hello',false)

我认为我们需要从这里开始:

Now, I want to apply Ramda to this module. I have never used this library before, but I have read that it is really cool, and makes the code easier to understand with some of its utilities.

恐怕这是把事情搞错了。 Ramda(免责声明:我是它的作者之一)的设计目的很简单:使以函数式编程 (FP) 方式编写起来更简单。它不是仿照 Underscore 或 lodash 的通用库。虽然它可以使您的代码更易于理解(我个人确实认为它很酷),但如果您不希望以 FP 风格编写代码,那么添加 Ramda 可能会适得其反。

既然如此,让我们假设您确实想开始转向 FP,而 Ramda 将是该旅程的第一步。然后让我们看一下代码。主要功能类似于

const getUserDataByUsername (username, cached = true) => { /* ... */ }

Ramda 非常重视易于组合到管道中的柯里化函数。这意味着可选参数几乎不可能很好地处理。通常在 Ramda 中,当我们使用默认的可选参数时,我们会创建一个需要这些值的函数,然后在它之上构建另一个部分应用默认值的函数。它可能看起来像这样:

const userByName = (cached) => (username) => { /* ... */ }
const getUserDataByUsername = userByName (true)

随着 Ramda 的柯里化,我们也可以这样写

const userByName = curry ((cached, username) => { /* ... */ })

我们现在有两个函数,userByName 更通用,但它需要您提供 cached 变量。 getUserDataByUsername 更简单,只需要 username.

不过,为了做到这一点,我们还需要更改 memoizedGetUserData,它具有类似 (userId, cached = true) => { /* ... */ } 的结构。同样,我们可以像这样手动咖喱它:

const memoizedGetUserData = (cached) => (userId) => { /* ... */ }

或者使用 Ramda 的柯里化:

const memoizedGetUserData = curry ((cached, userId) => { /* ... */ })

请注意,在这两种情况下,我们都将默认的可选参数从签名的末尾移到了开头。 Ramda 围绕柯里化和偏应用的设计意味着我们总是将参数从最不可能改变到最有可能改变的顺序排列。如果我们默认一个参数,我们显然相信它不太可能改变,所以它应该出现得更早。有人将其描述为“数据最后”,但我认为它比那更微妙。

现在让我们实现该主要功能。原来的样子是这样的:

async function getUserDataByUsername(username, cached = true) {
  const userId = await getUserId(username);

  return memoizedGetUserData(userId, cached);
}

以下是我可能首先考虑的重构方式:

const userByName = (cached) => (username) => getUserId (username) .then (memoizedGetUserData (cached))

它本身可能没问题。只有两步。管道可能是不必要的,但为了练习,让我们看看如何为此编写管道。首先,请注意 pipePcomposeP,具体的 Promise 流水线函数,已经被弃用了一段时间,取而代之的是更通用的 pipeWith/composeWith functions paired with andThen, and in fact, they have been removed entirely from the just-released version.

所以,使用 pipeWith 它可能看起来像这样:

const userByName = (cached) => pipeWith (andThen, [getUserId, memoizedGetUserData (cached)])

这应该与上面的行为完全相同。

我个人可能会到此为止。但是很多人喜欢他们的功能程序大多是无点的。我不确定这里有什么充分的理由,但如果我们想这样做,我们可以更进一步,将其写为

const userByName = pipe (memoizedGetUserData, andThen, flip (o) (getUserId))

我觉得这个版本不是很有吸引力,我会选择前两个版本中的一个,但它们中的任何一个都应该有效。

您可以通过展开此代码段来查看一些依赖项的虚拟版本:

// dummy
const memoizedGetUserData = curry ((cached, userId) => ({id: 123, first: 'Fred', last: 'Flintstone'}))

async function getUserId(username) {
  const usernameRef = firestore
    .collection("usernames")
    .doc(username.toLowerCase());

  const usernameDoc = await usernameRef.get();

  if (!usernameDoc.exists) {
    throw userErrors.userNotFound();
  }

  const { userId } = usernameDoc.data();

  return userId;
}

const userByName = (cached) => pipeWith (andThen, [getUserId, memoizedGetUserData (cached)])
// or
// const userByName = (cached) => (username) => getUserId (username) .then (memoizedGetUserData (cached))
// or
// const userByName = compose (flip (o) (getUserId), andThen, memoizedGetUserData)

const getUserDataByUsername = userByName (true)

getUserDataByUsername ('fred') 
  .then (console .log) 
  .catch (console .warn)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> 
const {curry, andThen, pipeWith} = R
// Dummy versions
const firestore = {collection: (name) => ({doc: () => ({get: () => Promise.resolve ({exists: () => true, data: () => ({id: 123, first: 'Fred', last: 'Flintstone'})})})})}
const userErrors = {userNotFound: () => 'oops'}
</script>