[TS 类型体操] 大数斐波那契&在wasm中的应用

534 阅读6分钟

简介

本文第一部分主要是实现大数版减法和斐波那契数列的计算, 第二部分介绍类型体操在实际项目中的应用, 以wasm中的cwrap/ccall函数为例子, 想保留发量的同学可以看应用部分, 水平有限, 要是有理解和表达不恰当的欢迎评论交流哈~ 实现效果如下 fib cwrap 当时之所以想实现fib是因为一般图灵完备的语言都可以实现, 并且一般能实现fib的也就可以看做是图灵完备的, 因为至少拥有了顺序, 循环和分支. 像HTML/CSS就不行(用css变量有没有搞头?) 【🤦♂️工作无用】证明 JS 和 TS 类型编程是图灵完备的 - 掘金

原始版本

绕了一些弯路, 但是基本思想还是用不同长度的元组代表数字, 数的表示可以用任意形式, 比如元组的嵌套深度或者字符串的长度, 这里主要是为了可读性选择元组 用ts类型系统实现斐波那契数列 - 掘金

简单版本1

主要思想是将减法 a-b = c, 转换为 a = b + c 的形式

// @ts-nocheck
type Len<T extends any[]> = T["length"];
// @ts-ignore
type IncList<T extends any[]> = [any, ...T];
// @ts-ignore
type AddList<A extends any[], B extends any[]> = [...A, ...B];
type FromLen<T extends number, A extends any[] = []> = Len<A> extends T
  ? A
  : FromLen<T, IncList<A>>;
export type Add<A extends number, B extends number> = Len<
  AddList<FromLen<A>, FromLen<B>>
>;
type SubList<A extends any[], B extends any[], C extends any[] = []> = Len<
  A
> extends Len<AddList<B, C>>
  ? C
  : SubList<A, B, IncList<C>>;

export type Sub<A extends number, B extends number> = Len<
  SubList<FromLen<A>, FromLen<B>>
>;

export type Fib<N extends number> = N extends 0
  ? 0
  : N extends 1
  ? 1
  : // @ts-ignore
    Add<Fib<Sub<N, 1>>, Fib<Sub<N, 2>>>;

简单版本2

去除了一些没必要的代码, 使用infer实现了减一和减二, 因为我们默认是大减去小 , 不会出现不够减的情况

// 辅助函数,暂时不用关心
type NumberToArray<T, I extends any[] = []> = T extends T ? I['length'] extends T ? I : NumberToArray<T, [any, ...I]> : never;
type Add<A, B> = [...NumberToArray<A>, ...NumberToArray<B>]['length']
type Sub1<T extends number> = NumberToArray<T> extends [infer _, ...infer R] ? R['length'] : never;
type Sub2<T extends number> = NumberToArray<T> extends [infer _, infer __, ...infer R] ? R['length'] : never;

// 计算斐波那契数列
type Fibonacci<T extends number> = 
  T extends 1 ? 1 :
  T extends 2 ? 1 :
  Add<Fibonacci<Sub1<T>>, Fibonacci<Sub2<T>>>;
  
  type Fibonacci9 = Fibonacci<9>;
  /** 得到结果
  type Fibonacci9 = 34
  */

限制

上述的实现本质上还是依赖于动态生成的元组, 但是由于递归深度问题, 即使是4.5优化了尾递归, 依然无法计算比较大的项 , ts4.4不到100就g, 4.5达不到1000, 所以展示案例一般都是fib(10) 因为这是4.4支持的最大数字了

type NumberToArray<T, I extends any[] = []> = T extends T ? I['length'] extends T ? I : NumberToArray<T, [any, ...I]> : never;​

type r1 = NumberToArray<100>​
type r2 = NumberToArray<1000>

大数版本

之前看到有同学实现了大数加法, 就想照此思路进行一些改进

大数加法

基本思想用打表实现单位数字加法, 然后用元组表示多位数, 用类型系统实现了一个加法器 用 TS 类型系统实现大数加法 - 知乎

大数减法

有了加法基本上就可以写操作系统了🐶 这里用补码实现减法, 在加法的基础上加了一点点工具函数(这里使用数字的高位在数组高位, 但是字符串中为了可读性使用的是数字高位在字符串低位) 默认大减去小, 不需要考虑负数情况, 所以小数字的补码需要和大数字位数一致, 然后要考虑高位有多余的0时需要去除

type ComplementMap = [9, 8, 7, 6, 5, 4, 3, 2, 1];
type DigitListToComplement<
  T extends any[],
  MAX extends any[],
  R extends any[] = []
> = T extends [infer First, ...infer Rest]
  ? DigitListToComplement<
      Rest,
      [...R, First extends number ? ComplementMap[First] : "N"]
    >
  : R;
type ToSameLen<
  T extends any[],
  MAX extends any[],
  R extends any[] = []
> = MAX["length"] extends 0 ? R : ToSameLen<Shift<T>, Shift<MAX>, [...R, T[0]]>;

type isNever<T extends any> = T extends never
  ? never extends T
    ? true
    : false
  : false;
type isUndefined<T extends any> = T extends undefined
  ? undefined extends T
    ? true
    : false
  : false;
type isUnknown<T extends any> = T extends unknown
  ? unknown extends T
    ? true
    : false
  : false;

type ToComplement<T extends any> = {
  [k in keyof T]: isNever<T[k]> extends true
    ? 9
    : isUndefined<T[k]> extends true
    ? 9
    : ComplementMap[T[k]];
};

type TrimRightZero<T extends any[]> = T extends [0]
  ? [0]
  : T extends [infer N]
  ? [N]
  : T extends [infer P, ...infer W, 0]
  ? TrimRightZero<[P, ...W]>
  : T;

type SubDigitList<A extends any[], B extends any[]> = TrimRightZero<
  Pop<AddDigitList<AddDigitList<A, ToComplement<ToSameLen<B, A>>>, [1]>>
>;

Fib

有了加减法其实Fib很简单了, 就是加减的组合, 最后再对string和元组包装一层就可以了

type FibDigitList<T extends any[] = []> = T extends [0]
  ? [0]
  : T extends [1]
  ? [1]
  : AddDigitList<
      FibDigitList<SubDigitList<T, [1]>>,
      FibDigitList<SubDigitList<T, [2]>>
    >;
 
type Fib<s extends string> = DigitListToString<FibDigitList<ToDigitList<s>>>;

declare function fib<T extends string>(n: T): Fib<T>;

const f50 = fib("50");

完整代码

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript

为wasm中的ccall和cwrap实现类型推断

完整代码参考 emscripten , 可以安装 @types/emscripten - npm 为项目中的wasm加上类型支持, 使用中有问题也欢迎提PR一起改进wasm的开发体验

原始定义

可以看到原始版本直接用的any, 这样就丢失了类型信息, 可能会导致参数顺序传错的问题o(╥﹏╥)o

declare function ccall(
    ident: string,
    returnType: Emscripten.JSType | null,
    argTypes: Emscripten.JSType[],
    args: Emscripten.TypeCompatibleWithC[],
    opts?: Emscripten.CCallOpts,
): any;
declare function cwrap(
    ident: string,
    returnType: Emscripten.JSType | null,
    argTypes: Emscripten.JSType[],
    opts?: Emscripten.CCallOpts,
): (...args: any[]) => any;

类型映射 主要目的是将字符串字面量'number' 映射为类型 number, 这里直接用一个map就行了

export type DataMap<R extends any> = R extends Emscripten.JSType
  ? {
      number: number;
      string: string;
      array: number;
      boolean: boolean;
    }[R]
  : never;

返回值推断

返回值是单个类型, 推断其实相当于从Map取对应的类型

type CvtReturn<R extends Emscripten.JSType | null> = R extends null
  ? null
  : DataMap<Exclude<R, null>>;

参数推断

这里坑了好久, 参数接收的是一个字符串字面量的元组, 返回的是对应的类型元组, 一切都很美好

export type CvtArg<T extends readonly Emscripten.JSType[]> = Extract<
  {
    [P in keyof T]: DataMap<T[P]>;
  },
  any[]
>;

然而当你实现后发现有问题. 用个简单的例子说明下, 可以发现类型 退化 了, 从字面量元组变成了字符串数组, 所以导致后续的推导失败, 这里不太清楚是什么机制导致的, 有同学知道的话欢迎交流哈 cwrap

export declare function cwrap<
  I extends readonly Emscripten.JSType[]
  R extends Emscripten.JSType | null
>(
  ident: string,
  returnType: R,
  argTypes: I,
  opts?: Emscripten.CCallOpts
): (...arg: CvtArg<I>) => CvtReturn<R>;

也想了一些办法, 但都没能解决, 后来在大佬的提醒下去看了NAPI的实现, 里面类似的函数, 经过一番摸索, 发现他们有一个很奇怪的操作, 虽然看不懂, 但确实能解决问题, 要是有能看懂的同学, 也欢迎交流哈 他们加了一个空数组(元组?), 然后就居然可以推导出来了 ???!!!

ccall

其实是cwrap的变种, 多了一个实参列表, 这里实参也是有类型约束的, 妈妈再也不用担心我传错参数了(o(╥﹏╥)o (就是因为有次传错了参数导致了一个极其诡异的问题, 所以才有了对wasm加类型的想法)

export declare function ccall<
  I extends readonly Emscripten.JSType[] | [],
  R extends Emscripten.JSType | null
>(
  ident: string,
  returnType: R,
  argTypes: I,
  args: CvtArg<I>,
  opts?: Emscripten.CCallOpts
): CvtReturn<R>;

个人感想

遇到卡住很久的问题最好请教大佬或者参考业界已有实现, 不然自己折腾有时候很费时间 其实对于类型体操可能大部分开发者都不会实际使用到, 但是类型体操能够让我们明确TS拥有怎样的能力, 以及能够解决怎样的问题, 当我们真正遇到复杂的推断时能够想到可以使用TS解决而不是直接用any, 体操的最终目的还是提升开发体验和减少bug