打字稿:为类似 reducer 的函数输入一个组合器

问题描述

考虑以下类型

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的类型,使得

  1. 常量 reducer 的类型为 Reducer<"S",{ first: "whatever",second: 5 }>
  2. 在标有 // *1 的行的范围内,firstsecond 的类型被正确推断为 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),firstsecond 的类型没有被推断正确。我们只得到 first: anysecond: any,而它应该是 first: 1second: 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
    },})

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...