Создание калькулятора в Typelevel из TypeScript

Одной из самых древних частей человечества, созданных для выполнения арифметических действий, является абак, а его современной версией — калькулятор. В этом посте я проведу экскурс по одному из них, созданному на основе системы типов TypeScript.

Прежде чем я начну, я поблагодарю @noghartt за то, что он показал мне эту сенсационную вики и очень помог мне в строительстве.

Я предполагаю, что вы прочитали вводный блогпост по этой ссылке.

Начало

Давайте начнем с некоторых правил, одно из которых заключается в том, что мы находимся в десятичной системе. Итак, чтобы перейти к следующему числу, мы прибавим +1 или отнимем -1 и благодаря его позиции узнаем, является ли это число десятками, сотнями или тысячами.

Для начала создадим сложение единицы и вычитание единицы:

type Reverse<A> =
  `${A}` extends `${infer AH}${infer AT}`
    ? `${Reverse<AT>}${AH}` : A;

type Digs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

type DigsNext<I = Digs, R = {}> =
  I extends [infer Head, infer Next, ...infer Tail]
    ? DigsNext<[Next, ...Tail], R & Record<Head, Next>>
    : { [K in keyof R]: R[K] };

type DigsPrev = { [K in keyof DigsNext as DigsNext[K]]: K };

type ToNumber<
  S extends string,
  L extends number[] = []>
  = `${L['length']}` extends S
  ? L['length']
  : ToNumber<S, [...L, 0]>;

type GreaterThan<
  T extends number,
  U extends number,
  C extends unknown[] = []
  > =
  T extends U
    ? false
    : C['length'] extends T
      ? false
      : C['length'] extends U
        ? true
        : GreaterThan<T, U, [...C, 1]>;

type AddOne<A> =
  A extends `${infer AH}${infer AT}`
    ? AH extends '9' ? `0${AddOne<AT>}` : `${DigsNext[AH]}${AT}`
    : `1`
type SubOne<A> =
  A extends `${infer AH}${infer AT}`
    ? AH extends '0' ? `9${SubOne<AT>}` : `${DigsPrev[AH]}${AT}`
    : never
Войдите в полноэкранный режим Выход из полноэкранного режима

И это все?

К удивлению многих, практика после создания этих базовых функций сводится к их составлению и рекурсии, т.е. повторению N раз суммы +1 до получения «нормальной» суммы или N раз вычитания для получения «нормального» вычитания.

type Sub<
  A extends string,
  B extends string,
  R extends string = "0"
  > =
  B extends R
    ? A
    : Sub<SubOne<A>, B, AddOne<R>>;

type Add<A, B> =
  A extends `${infer AH}${infer AT}` ?
    B extends `${infer BH}${infer BT}`
      ? BH extends '0' ? `${AH}${Add<AT, BT>}` : Add<AddOne<A>, SubOne<B>>
      : A : B;
Войдите в полноэкранный режим Выход из полноэкранного режима

Создание суммы и вычитания, как описано выше

Рекурсия

Чтобы получить операции умножения и деления, мы должны рекурсивно перебирать сложение и вычитание, пока не получим результат. Это можно увидеть в приведенном ниже фрагменте:

type Mul<A, B, R = '0'> =
  A extends '0' ? R :
    B extends '0' ? R :
      A extends `${infer AH}${infer AT}`
        ? AH extends '0' ? Mul<AT, `0${B}`, R> : Mul<SubOne<A>, B, Add<R, B>>
        : R;

type Multiply<A extends string | number | bigint, B extends string | number | bigint> =
  Reverse<Mul<Reverse<A>, Reverse<B>>>;

type Division<
  W extends string,
  D extends string,
  Q extends string = '0'
  > =
  W extends '0'
    ? Q
    : Division<Sub<W, D>, D, AddOne<Q>>;
Войдите в полноэкранный режим Выход из полноэкранного режима

Веселье

Играя с рекурсией и композицией этих первичных функций, мы получаем некоторые другие операции, такие как Power и Log:

type Power<
  V extends string,
  P extends string,
  A extends string = V> =
  P extends '1'
    ? A
    : P extends '0'
      ? '1'
      : Power<V, SubOne<P>, Multiply<V, A>>;

// Log<10, 100> = 2
type Log<
  B extends string,
  L extends string,
  I extends string = "0",
  PA extends string = "0"> =
  L extends "1"
    ? "0"
    : L extends PA
      ? I
      : GreaterThan<ToNumber<PA>,ToNumber<L>> extends true
        ? never
        : Log<B, L, AddOne<I>, Power<B, AddOne<I>>>;

const $: Power<'2', '4'> = "16";
const _: Power<'2', '3'> = "8";
Войдите в полноэкранный режим Выход из полноэкранного режима

Результат

Как вы можете видеть в этом калькуляторе и начиная с нуля, вы можете создавать математику в любой среде, где есть возможность создавать переменные, рекурсию/итерацию и управлять потоком (if’s). На этом калькулятор заканчивается:

type OpDict<A extends string, B extends string>  = {
  'Div': Division<A, B>,
  'Mul': Multiply<A, B>,
  'Sum': Add<A, B>,
  'Sub': Sub<A, B>,
  'Log': Log<A, B>,
  'Pow': Power<A, B>
};

type Calculator
  <Operation extends keyof OpDict<string, string>,
    A extends string = '0',
    B extends string = '0'
    > = OpDict<A, B>[Operation];

const teste: Calculator<'Mul', '2', '2'> // 4
const teste: Calculator<'Mul', '2', '8'> // 16
const teste: Calculator<'Sum', '2', '4'> // 6
const teste: Calculator<'Sub', '9', '4'> // 5
Войдите в полноэкранный режим Выход из полноэкранного режима

На этом мы подходим к завершению еще одной саги. На этот раз он включал в себя немного меньше концепций и немного больше работы в смысле преобразования абстрактных идей в реальные функции.

Хотите посмотреть, как это работает на вашей машине? Ссылка на ts plaground и gist

Оцените статью
devanswers.ru
Добавить комментарий