问题描述
考虑以下类型
type Reducer<S,A> = Red<S,A,A>
type Red<S,C,A> = (s: S) => (c: C) => A
和下面的函数
// The type intended for `reds` is
// `{ [k in keyof P]: Red<S,P,P[k]> }`
// for fixed `S` and `P`
const combinator: ??? = reds =>
step => acc =>
{
const result = {}
for (const key in reds) {
result[key] = reds[key](step)(acc)
}
return result
}
现在考虑这个函数的以下应用(以 ts 为单位):
const reducer = combinator({
first: (s: "S") => ({ first,second }) => {
// *1
return "whatever"
},second: (s: "S") => ({ first,second }) => {
// *1
return 5
},})
问题:我们如何在打字稿中定义函数combinator
的类型,使得
- 常量
reducer
的类型为Reducer<"S",{ first: "whatever",second: 5 }>
- 在标有
// *1
的行的范围内,first
和second
的类型被正确推断为first: "whatever"
和second: 5
简而言之:我们应该在下面的代码中添加什么来代替 ???
const combinator: ??? = reds => { ... }
[备注]:我发布了部分解决方案。也许它有助于作为起点?
编辑:
组合器的动机是允许使用相互引用的减速器进行并行归约。我想到的用例是编译 AST。但为了简单起见:Here's a contrived example:
const combinator = reds =>
step => acc =>
{
const result = {}
for (const key in reds) {
result[key] = reds[key](step)(acc)
}
return result
}
const reduce = arr => ({ init,red }) =>
{
let acc = init
for (const step of arr) {
acc = red(step)(acc)
}
return acc
}
const exampleInput = [
"some line","some other line","yet another line",]
const exampleReducer = combinator({
textWithLengths:
line => ({ textWithLengths,totalLength }) =>
textWithLengths + "\n" + `${totalLength} -- ${line}`,totalLength:
line => ({ totalLength }) =>
totalLength + line.length,startsWithSame:
line => ({ startsWithSame,previousLine }) =>
[...startsWithSame,previousLine.charAt(0) === line.charAt(0)],previousLine:
line => _ =>
line
})
const exampleInit = {
textWithLengths: "",totalLength: 0,startsWithSame: [],previousLine: "",}
const exampleResult = reduce(exampleInput)({
init: exampleInit,red: exampleReducer
})
console.log(exampleResult.textWithLengths)
console.log(exampleResult.startsWithSame)
输出将是
// console.log(exampleResult.textWithLengths)
0 -- some line
9 -- some other line
24 -- yet another line
[ false,true,false ]
// console.log(exampleResult.startsWithSame)
[ false,false ]
解决方法
我认为类型的定义有点复杂。 鉴于其他代码,我不知道您是否有“附带”要求,但是,在真空中进行评估,这样的事情可以工作(未经测试,类型强制除外):
const exampleInput = [
"some line","some other line","yet another line",]
interface Aggr {
textWithLengths: string;
totalLength: number;
startsWithSame: boolean[];
previousLine: string;
}
const exampleInit: Aggr = {
textWithLengths: "",totalLength: 0,startsWithSame: [],previousLine: "",}
type Reducers<TInput,TOutput> = {
[k in keyof TOutput]: (input: TInput,aggr: TOutput) => TOutput[k];
};
const reduce = function <TInput,TOutput>(
input: TInput[],init: TOutput,reducers: Reducers<TInput,TOutput>
) {
let acc = init;
const reducerNames = Object.keys(reducers);
for (const item of input) {
const newAcc: any = {};
for (const reducerName of reducerNames) {
const red = (reducers as any)[reducerName];
newAcc[reducerName] = red(item,acc);
}
acc = newAcc as TOutput;
}
return acc;
};
// types can be implicitly checked
const result = reduce<string,Aggr>(exampleInput,exampleInit,{
textWithLengths: (line,aggr) =>
aggr.textWithLengths + "\n" + `${aggr.totalLength} -- ${line}`,totalLength: (line,aggr) => aggr.totalLength + line.length,startsWithSame: (line,aggr) => [
...aggr.startsWithSame,aggr.previousLine.charAt(0) === line.charAt(0),],previousLine: (line) => line,});
console.log(result.textWithLengths)
console.log(result.startsWithSame)
您可以使用另一个接口来代替 TInput[]
,例如 Iterator<TInput>
。
编辑:我更新了修复 reduce 内部某些类型不匹配的代码并对其进行了测试 here。我觉得还好。
,Here's a partial solution。但这不是完整的答案:问题是在代码中标记的地方(在 *1
、*2
),first
和 second
的类型没有被推断正确。我们只得到 first: any
和 second: any
,而它应该是 first: 1
和 second: 2
。
type Red<S,C,A> = (s: S) => (c: C) => A
type Reducer<S,A> = Red<S,A,A>
type Target<R extends Red<any,any,any>> =
R extends Red<any,infer S>
? S
: ["this should not happen: could not infer target type of reducer",R]
type CombinedTarget<Reds extends { [k: string]: Red<any,any> }> =
{ [k in keyof Reds]: Target<Reds[k]> }
const combinator = <S,Reds extends { [k: string]: Red<S,any> }>(
reds: Reds
): Reducer<S,CombinedTarget<Reds>> =>
(s: S) => (p: CombinedTarget<Reds>) =>
{
const result: Partial<CombinedTarget<Reds>> = {}
for (const key in reds) {
result[key] = reds[key](s)(p)
}
return result as CombinedTarget<Reds>
}
const result = combinator({
first: (s: "S") => ({ first,second }) => {
// *1
return 1 as 1
},second: (s: "S") => ({ first,second }) => {
// *2
return 2 as 2
},})