20 de junho de 2022 • 8 min de leitura
Fazendo sua calculadora em Typelevel do TypeScript
Poucos gists me deixam tão orgulhoso quanto ter feito uma calculadora em typelevel. Criar ferramentas de aritmética onde não se tem é uma taréfa ardua
Uma das peças mais antigas da humanidade feita para fazer aritmética é o abaco e sua versão moderna é a calculadora. Nesse blogpost irei fazer uma tour por uma feita a partir o sistema de tipos do TypeScript.
Antes de começar farei um agradecimento ao @noghartt por me mostrar essa wiki sensacional e me ajudar bastante nessa construção.
Vou supor que você tenha lido o blogpost de introdução ao assunto link
O começo
Vamos partir de algumas regras, uma delas é que estamos em um sistema decimal. Sendo assim, para chegar no próximo número iremos adicionar +1 ou remover -1 e graças a sua posição saberemos se trata-se de um número em dezenas, centenas ou milhares.
Para começar vamos criar a soma de um e subtração de um:
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
É só isso ?
Para a surpresa de muitos a mátematica depois da criação dessas funções base nada mais é do que a composição e recursão delas, ou seja, repetir N vezes a soma de +1 até ter uma soma 'normal' ou N vezes a subtração para obter a subtração 'normal'
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;
Criação da soma e da subtração como descrita acima
Recursão
Para obtermos as operações de multiplicação e divisão devemos fazer a recursão da soma e da subtração recursivamente até obtermos nosso resultado. O que pode ser observado no snippet abaixo:
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>>;
Diversão
Brincando com recursão e a composição dessas funções primordiais obtemos mais algumas operações como as de potencia e de 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";
Resultado
Como pode ser observado nessa calculadora e partindo do zero pode-se criar a matématica em qualquer ambiente que seja póssivel criar variáveis, recursão/ iteração e controle de fluxo(if's). O final da caluladora é esse:
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
Com isso chegamos na conclusão de mais uma saga. Dessa vez envolveu um pouco menos de conceitos e sim um pouco mais de trabalho no sentido de transformar ideias abstratas em funções reais.
Quer ver rodando em sua máquina ? link para plaground do ts e gist
Contato
Se quiser discutir sobre qualquer assunto ou viu algum erro, não hesite em me marcar ou chamar no Twitter: @que_cara_legal
Estou sempre tentando trazer o que tenho estudado, as vezes traduzindo algums tópicos divertidos que me chamam atenção.