如何在映射时合并两个数组?
How to merge two arrays while mapping?
背景:我对 Ramda 和 FP 还很陌生。我发现自己 运行 进入了这个场景,我有两个输入 itemList
和 costList
。列表中的值具有基于 idx 的关系。因此 itemList[0]
和 costList[0]
表示可能在同一对象中的值(例如 { item: itemList[0], cost: costList[0]}
)。因此,替代解决方案可以从合并 itemList 和 costList 开始。我对其中一个或两个解决方案感兴趣。任何其他提示和建议也将不胜感激。
let itemList = ['shirt', 'shoes'];
let costList = ['shirtCost', 'shoesCost'];
let formulaList = R.map(
x => ({
formulaXAttr: x,
formulaYAttr: 'tbd later',
})
)(itemList);
let finalList = R.map(
x => R.merge(x, {formulaYAttr: '???'}) // how to merge in costList?
)(formulaList);
// this is how i'd do it in vanilla JS
let finalList = formulaList.map((x, idx) => ({
...x,
formulaYAttr: costList[idx],
}));
您在 Ramda 中寻找的函数称为 zipWith
,它接受一个需要两个参数的函数,一个用于第一个列表的每个元素以及第二个列表中的成对元素。这最终是一个与所提供的两个列表中较短的一个一样长的列表,其中包含为每对调用函数的结果。
const itemList = ['shirt', 'shoes'];
const costList = ['shirtCost', 'shoesCost'];
const fn = R.zipWith((x, y) =>
({ formulaXAttr: x, formulaYAttr: y }))
console.log(
fn(itemList, costList)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
Scott Christopher 的回答显然是正确的。 zipWith
正是为这种场景设计的。在您的标题中,您询问如何在映射时执行此操作。我想指出的是,虽然 Ramda 确实为此提供了一个选项(有关详细信息,请参阅 addIndex
),但它通常不受欢迎。这样做的原因对于进一步了解 FP 很重要。
对map
的一种理解是,它通过将给定函数应用于每个元素,将一种类型的元素列表转换为另一种类型的元素列表。这是一个很好的公式,但还有一个更通用的公式:map
将一种类型的元素的 container 转换为 container通过将给定的函数应用到每个元素,另一种类型的元素。换句话说,mapping
的概念可以应用于许多不同的容器,而不仅仅是列表。 FantatsyLand specification defines some rules for containers which have map
methods. The specific type is Functor
,但对于没有相关数学背景的人来说,这可以认为是Mappable
。 Ramda 应该与这些类型很好地互操作:
const square = n => n * n;
map(square, [1, 2, 3, 4, 5]) //=> [1, 4, 9, 16, 25]
// But also, if MyContainer is a Functor
map(square, MyContainer(7)) //=> MyContainer(49)
// and, for example, if for some Maybe Functor with Just and Nothing subtypes
const {Just, Nothing} = {Maybe}
// then
map(square, Just(6)) //=> Just(36)
map(square, Nothing()) //=> Nothing()
Ramda 的 map
函数仅使用容器的元素调用提供的转换函数。它不提供索引。这是有道理的,因为并非所有容器都有索引的概念。
因此,映射是 Ramda 不是尝试匹配索引的地方。但这是 Ramda 的核心 three zip functions。如果你想合并两个元素与索引匹配的列表,你可以像 Scott 对 zipWith
所做的那样最简单。但您也可以使用 zip
,它的工作方式如下:
zip(['a', 'b', 'c', 'd'], [2, 3, 5, 7]) //=> [['a', 2], ['b', 3], ['c', 5], ['d', 7]]
随后对结果对调用 map
。 zipWith
只需一步即可完成同样的操作。但是没有它,像 map
和 zip
这样的原语仍然可以组合起来做你喜欢的事。
为了配合这里的其他答案,我想向您展示如何自己编写此类内容。 Ramda 库不会总是为您提供单行解决方案,如果您将解决方案限制为仅使用 Ramda 提供的功能,您就会退缩。
这种做法会让您有信心尝试编写自己的程序。 When/if 您发现 Ramda 具有您需要的一些功能,您可以轻松地重构您的程序以使用 Ramda 提供的功能。
const None =
Symbol ()
const zipWith = (f, [ x = None, ...xs ], [ y = None, ...ys ]) =>
x === None || y === None
? []
: [ f (x, y) ] .concat (zipWith (f, xs, ys))
console.log
( zipWith
( (name, price) => ({ name, price })
, [ 'shoes', 'pants' ]
, [ 19.99, 29.99 ]
)
)
// [ { name: "shoes", price: 19.99 }
// , { name: "pants", price: 29.99 }
// ]
上面使用的解构赋值创建有助于保持声明式风格,但它确实创建了不必要的中间值。下面,我们使用一个额外的状态参数 i
来显着减少内存占用。
const zipWith = (f, xs, ys, i = 0) =>
i >= xs.length || i >= ys.length
? []
: [ f (xs[i], ys[i]) ] .concat (zipWith (f, xs, ys, i + 1))
console.log
( zipWith
( (name, price) => ({ name, price })
, [ 'shoes', 'pants' ]
, [ 19.99, 29.99 ]
)
)
// [ { name: "shoes", price: 19.99 }
// , { name: "pants", price: 29.99 }
// ]
注意这两个实现都是纯的。没有副作用。输入不会发生变化,并且始终会为输出构建一个新数组。
我们的实现也是完整的,因为在提供有效输入时它总是会以有效输出作为响应。
console.log
( zipWith
( (name, price) => ({ name, price })
, [ 'shoes' ]
, [ 19.99 ]
)
// [ { name: "shoes", price: 19.99 } ]
, zipWith
( (name, price) => ({ name, price })
, [ 'shoes' ]
, []
)
// []
, zipWith
( (name, price) => ({ name, price })
, []
, [ 19.99 ]
)
// []
, zipWith
( (name, price) => ({ name, price })
, []
, []
)
// []
)
这次 Ramda 支持您 R.zipWith
,但下次可能不会。但是不要害怕,因为你得到了这个!一旦您意识到编程没有魔法,编写这样的程序就变得容易了。如果您遇到困难,我想总有 Whosebug。
背景:我对 Ramda 和 FP 还很陌生。我发现自己 运行 进入了这个场景,我有两个输入 itemList
和 costList
。列表中的值具有基于 idx 的关系。因此 itemList[0]
和 costList[0]
表示可能在同一对象中的值(例如 { item: itemList[0], cost: costList[0]}
)。因此,替代解决方案可以从合并 itemList 和 costList 开始。我对其中一个或两个解决方案感兴趣。任何其他提示和建议也将不胜感激。
let itemList = ['shirt', 'shoes'];
let costList = ['shirtCost', 'shoesCost'];
let formulaList = R.map(
x => ({
formulaXAttr: x,
formulaYAttr: 'tbd later',
})
)(itemList);
let finalList = R.map(
x => R.merge(x, {formulaYAttr: '???'}) // how to merge in costList?
)(formulaList);
// this is how i'd do it in vanilla JS
let finalList = formulaList.map((x, idx) => ({
...x,
formulaYAttr: costList[idx],
}));
您在 Ramda 中寻找的函数称为 zipWith
,它接受一个需要两个参数的函数,一个用于第一个列表的每个元素以及第二个列表中的成对元素。这最终是一个与所提供的两个列表中较短的一个一样长的列表,其中包含为每对调用函数的结果。
const itemList = ['shirt', 'shoes'];
const costList = ['shirtCost', 'shoesCost'];
const fn = R.zipWith((x, y) =>
({ formulaXAttr: x, formulaYAttr: y }))
console.log(
fn(itemList, costList)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
Scott Christopher 的回答显然是正确的。 zipWith
正是为这种场景设计的。在您的标题中,您询问如何在映射时执行此操作。我想指出的是,虽然 Ramda 确实为此提供了一个选项(有关详细信息,请参阅 addIndex
),但它通常不受欢迎。这样做的原因对于进一步了解 FP 很重要。
对map
的一种理解是,它通过将给定函数应用于每个元素,将一种类型的元素列表转换为另一种类型的元素列表。这是一个很好的公式,但还有一个更通用的公式:map
将一种类型的元素的 container 转换为 container通过将给定的函数应用到每个元素,另一种类型的元素。换句话说,mapping
的概念可以应用于许多不同的容器,而不仅仅是列表。 FantatsyLand specification defines some rules for containers which have map
methods. The specific type is Functor
,但对于没有相关数学背景的人来说,这可以认为是Mappable
。 Ramda 应该与这些类型很好地互操作:
const square = n => n * n;
map(square, [1, 2, 3, 4, 5]) //=> [1, 4, 9, 16, 25]
// But also, if MyContainer is a Functor
map(square, MyContainer(7)) //=> MyContainer(49)
// and, for example, if for some Maybe Functor with Just and Nothing subtypes
const {Just, Nothing} = {Maybe}
// then
map(square, Just(6)) //=> Just(36)
map(square, Nothing()) //=> Nothing()
Ramda 的 map
函数仅使用容器的元素调用提供的转换函数。它不提供索引。这是有道理的,因为并非所有容器都有索引的概念。
因此,映射是 Ramda 不是尝试匹配索引的地方。但这是 Ramda 的核心 three zip functions。如果你想合并两个元素与索引匹配的列表,你可以像 Scott 对 zipWith
所做的那样最简单。但您也可以使用 zip
,它的工作方式如下:
zip(['a', 'b', 'c', 'd'], [2, 3, 5, 7]) //=> [['a', 2], ['b', 3], ['c', 5], ['d', 7]]
随后对结果对调用 map
。 zipWith
只需一步即可完成同样的操作。但是没有它,像 map
和 zip
这样的原语仍然可以组合起来做你喜欢的事。
为了配合这里的其他答案,我想向您展示如何自己编写此类内容。 Ramda 库不会总是为您提供单行解决方案,如果您将解决方案限制为仅使用 Ramda 提供的功能,您就会退缩。
这种做法会让您有信心尝试编写自己的程序。 When/if 您发现 Ramda 具有您需要的一些功能,您可以轻松地重构您的程序以使用 Ramda 提供的功能。
const None =
Symbol ()
const zipWith = (f, [ x = None, ...xs ], [ y = None, ...ys ]) =>
x === None || y === None
? []
: [ f (x, y) ] .concat (zipWith (f, xs, ys))
console.log
( zipWith
( (name, price) => ({ name, price })
, [ 'shoes', 'pants' ]
, [ 19.99, 29.99 ]
)
)
// [ { name: "shoes", price: 19.99 }
// , { name: "pants", price: 29.99 }
// ]
上面使用的解构赋值创建有助于保持声明式风格,但它确实创建了不必要的中间值。下面,我们使用一个额外的状态参数 i
来显着减少内存占用。
const zipWith = (f, xs, ys, i = 0) =>
i >= xs.length || i >= ys.length
? []
: [ f (xs[i], ys[i]) ] .concat (zipWith (f, xs, ys, i + 1))
console.log
( zipWith
( (name, price) => ({ name, price })
, [ 'shoes', 'pants' ]
, [ 19.99, 29.99 ]
)
)
// [ { name: "shoes", price: 19.99 }
// , { name: "pants", price: 29.99 }
// ]
注意这两个实现都是纯的。没有副作用。输入不会发生变化,并且始终会为输出构建一个新数组。
我们的实现也是完整的,因为在提供有效输入时它总是会以有效输出作为响应。
console.log
( zipWith
( (name, price) => ({ name, price })
, [ 'shoes' ]
, [ 19.99 ]
)
// [ { name: "shoes", price: 19.99 } ]
, zipWith
( (name, price) => ({ name, price })
, [ 'shoes' ]
, []
)
// []
, zipWith
( (name, price) => ({ name, price })
, []
, [ 19.99 ]
)
// []
, zipWith
( (name, price) => ({ name, price })
, []
, []
)
// []
)
这次 Ramda 支持您 R.zipWith
,但下次可能不会。但是不要害怕,因为你得到了这个!一旦您意识到编程没有魔法,编写这样的程序就变得容易了。如果您遇到困难,我想总有 Whosebug。