如何将 Ramda 管道函数与承诺和静态回调混合使用?



基于@ScottSauyet的帮助,我已经能够创建一个函数来解决初始数据对象的静态和基于承诺的回调。

现在,我希望能够通过一系列回调来管道传输此数据对象,但是一旦我将多个承诺添加到组合中,就会遇到麻烦。

当前设置

// Libaries
const R = require('ramda');
const fetch = require('node-fetch');
const Promise = require('bluebird');
// Input
const data = {
array: [['#', 'FirstName', 'LastName'], ['1', 'tim', 'foo'], ['2', 'kim', 'bar']],
header: 'FirstName',
more: 'stuff',
goes: 'here'
};
// Static and Promise Resolver (with Helper Function)
const transposeObj = (obj, len = Object.values(obj)[0].length) =>
[...Array(len)].map((_, i) => Object.entries(obj).reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}));
const mergeCallback = async ({ array: [headers, ...rows], header, ...rest }, callback) => {
const index = R.indexOf(header, headers);
const result = await Promise.map(rows, row => {
return callback(row[index]);
})
.then(x => ({ changes: x.map(v => transposeObj(v.changes)) }))
.then(({ changes }) => ({
allHeaders: R.flatten([
...headers,
R.chain(t => R.chain(Object.keys, t), [...changes])
.filter(k => !headers.includes(k))
.filter((x, i, a) => a.indexOf(x) == i)
]),
changes
}))
.then(({ changes, allHeaders }) => ({
resultRows: R.chain(
(row, i = R.indexOf(row, [...rows])) =>
changes[i].map(change =>
Object.entries(change).reduce(
(r, [k, v]) => [...r.slice(0, allHeaders.indexOf(k)), v, ...r.slice(allHeaders.indexOf(k) + 1)],
row.slice(0)
)
),
[...rows]
),
allHeaders
}))
.then(({ resultRows, allHeaders, array }) => ({
array: [allHeaders, ...resultRows],
header,
...rest
}));
return result;
};
// Example Callbacks and their services
const adapterPromise1 = async name => {
const response = await fetch(`https://api.abalin.net/get/getdate?name=${name}&calendar=us`).then(res => res.json());
return {
changes: {
nameday: R.pluck('day', response.results),
namemonth: R.pluck('month', response.results)
}
};
};
const servicePromise1 = input => mergeCallback(input, adapterPromise1);
const adapterPromise2 = async name => {
const response = await fetch(`https://api.genderize.io?name=${name}`).then(res => res.json());
return {
changes: {
gender: R.of(response.gender)
}
};
};
const servicePromise2 = input => mergeCallback(input, adapterPromise2);
const adapterStatic1 = name => ({ changes: { NameLength: R.of(R.length(name)) } });
const serviceStatic1 = input => mergeCallback(input, adapterStatic1);

管道尝试

const result = R.pipe(
servicePromise1,
servicePromise2,
serviceStatic1
)(data);
// console.log(result); <<< preferred resolution method, but not working due to promise
result.then(console.log);

预期成果

{ array:  
[ [ '#', 
'FirstName', 
'LastName', 
'nameday', 
'namemonth', 
'gender', 
'NameLength' ], 
[ '1', 'tim', 'foo', 24, 1, 'male', 3 ], 
[ '1', 'tim', 'foo', 20, 6, 'male', 3 ], 
[ '2', 'kim', 'bar', 8, 9, 'male', 3 ], 
[ '2', 'kim', 'bar', 11, 10, 'male', 3 ] ], 
header: 'FirstName', 
more: 'stuff', 
goes: 'here' } 

当前结果

Pipe 适用于任何一个服务调用,但一旦我尝试使用两个或多个服务,就会收到以下错误。

Cannot read property 'Symbol(Symbol.iterator)' of undefined 

任何关于如何使其工作的建议将不胜感激。

Ramda的pipe不是Promise感知的。 旧的 Promise 感知版本pipeP被弃用,取而代之的是更通用的pipeWith。 您可以通过传递R.then(即将重命名为R.andThen)将其与 Promise 一起使用,如下所示:

R.pipeWith (R.then, [
//servicePromise1, // problem with CORS headers here.
servicePromise2,
serviceStatic1
]) (data)
.then (console .log)

出于某种原因,当我尝试从 Ramda 的 REPL 或 SO 代码段运行它时,您的第一个 API 调用遇到了我的 CORS 问题,但没有它,这个过程应该很清楚。

这可能足以解决您的问题。 它适用于此测试用例。 但我看到了一个悬而未决的问题:所有版本的pipe都将上一个调用的结果传递到下一个调用。 但是,您可以使用数据的属性来配置有关如何触发下一个回调的内容,即header属性。 因此,这必须在整个管道中保持固定。 如果所有调用都将使用FirstName属性,那很好,但我的印象是他们需要自己的版本。

但是,编写一个自定义管道函数很容易,该函数允许您将其与回调函数一起传递。 然后,您的呼叫可能如下所示:

seq ([
['FirstName', servicePromise2],
['FirstName', serviceStatic1]
]) (data)
.then(console.log)

您可以在此代码片段中看到该想法的工作版本:

// Input
const data = {
array: [['#', 'FirstName', 'LastName'], ['1', 'tim', 'foo'], ['2', 'kim', 'bar']],
header: 'FirstName',
more: 'stuff',
goes: 'here'
};
// Static and Promise Resolver (with Helper Function)
const transposeObj = (obj, len = Object.values(obj)[0].length) =>
[...Array(len)].map((_, i) => Object.entries(obj).reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}));
const mergeCallback = async ({ array: [headers, ...rows], header, ...rest }, callback) => {
const index = R.indexOf(header, headers);
const result = await Promise.all(rows.map(row => {
return callback(row[index]);
}))
.then(x => ({ changes: x.map(v => transposeObj(v.changes)) }))
.then(({ changes }) => ({
allHeaders: R.flatten([
...headers,
R.chain(t => R.chain(Object.keys, t), [...changes])
.filter(k => !headers.includes(k))
.filter((x, i, a) => a.indexOf(x) == i)
]),
changes
}))
.then(({ changes, allHeaders }) => ({
resultRows: R.chain(
(row, i = R.indexOf(row, [...rows])) =>
changes[i].map(change =>
Object.entries(change).reduce(
(r, [k, v]) => [...r.slice(0, allHeaders.indexOf(k)), v, ...r.slice(allHeaders.indexOf(k) + 1)],
row.slice(0)
)
),
[...rows]
),
allHeaders
}))
.then(({ resultRows, allHeaders, array }) => ({
array: [allHeaders, ...resultRows],
header,
...rest
}));
return result;
};
// Example Callbacks and their services
const adapterPromise2 = async (name) => {
const response = await fetch(`https://api.genderize.io?name=${name}`).then(res => res.json());
return {
changes: {
gender: R.of(response.gender)
}
};
};
const servicePromise2 = input => mergeCallback(input, adapterPromise2);
const adapterStatic1 = name => ({ changes: { NameLength: R.of(R.length(name)) } });
const serviceStatic1 = input => mergeCallback(input, adapterStatic1);
const seq = (configs) => (data) =>
configs.reduce(
(pr, [header, callback]) => pr.then(data => callback({...data, header})),
Promise.resolve(data)
)
seq ([
['FirstName',  servicePromise2],
['FirstName', serviceStatic1]
]) (data)
.then(console.log)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

不过,我仍然认为这有些尴尬。 您要向我查找的标头名称根本不属于该输入数据。 你可以让它成为mergeCallback函数的另一个属性,并更新你的包装器以从那里传递它,比如

const servicePromise2 = (input) => mergeCallback(input, 'FirstName', adapterPromise2);

在我看来更好的是,即使我知道它会为您现有的回调函数增加一些工作,也会将整行传递给回调函数,该函数结构为对象,所有标头都作为属性。 Ramda的zipObj可以这样使用:

const result = await Promise.all(rows.map(row => {
return callback(zipObj(headers, row));
}))

像这样传递给每个回调对象:

{"#":"1", FirstName: "tim", LastName: "foo" /*, gender: 'male', ... */}

您可以将回调的签名更改为如下所示

const adapterPromise2 = async ({FirstName: name}) => { ...use `name` ... }

并保持主体不变,或者只需将变量名称更改为FirstName以匹配对象。

const adapterPromise2 = async ({FirstName}) => { ...use `FirstName`... }

无论哪种方式,这都会使通用代码更简单,删除在当前 API 中感觉非常尴尬的header属性,而无需显着更改现有回调。

最新更新