programação funcional - Parâmetro de tipo genérico e monads em TypeScript
Atualmente, estou aprendendo sobre mónadas - e tentando usar TypeScript para entendê-las melhor.
Eu gostaria de fazer o seguinte:
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
type Unit<M, A> = (a: A) => M<A>
Mas estou recebendo os seguintes erros do TypeScript:
error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
~~~~
error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
~~~~
error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
~~~~
error TS2315: Type 'M' is not generic.
type Unit<M, A> = (a: A) => M<A>
~~~~
Existe uma forma de especificar que o parâmetro de tipo M deve ser genérico? Ou há outra maneira sensata de fazer isso?
A ideia é que eu possa, mais tarde, passar por exemplo Maybe no lugar de M, onde Maybe é um tipo genérico:
type Maybe<T> = { kind: 'nothing' } | { kind: 'some', value: T }
Resposta #1
Não, o TypeScript não oferece suporte direto a tipos de ordem superior do tipo necessário para definir Bind ou Unit. Há uma solicitação de recurso aberta há muito tempo em microsoft/TypeScript#1213 pedindo por isso, e não está claro quando ou se isso algum dia será implementado.
Atualmente, o TypeScript não possui a capacidade de abstrair sobre funções de tipo. (Uma função de tipo age sobre tipos da mesma forma que uma função regular age sobre valores; ela recebe alguns tipos de entrada e produz um tipo de saída.) Ela pode representar funções de tipo concretas individuais como Array, e você pode definir novas funções de tipo concretas individuais como:
type Foo<T> = {x: T} // Foo is a type function
mas você não pode falar sobre funções de tipo em geral (não existe um Foo por si só para referenciar), nem pode passar funções de tipo para outras funções de tipo de ordem superior. Como seu parâmetro de tipo M precisa ser uma função de tipo arbitrária para que isso funcione, não há como fazer isso diretamente.
Observe que eu disse que não suporta diretamente tipos de ordem superior. É possível emular tipos de ordem superior, mas todas as maneiras de fazê-lo que eu conheço são complicadas e exigem muita intervenção manual e código boilerplate. Eu não diria que isso é "insano", mas também não diria necessariamente que seja "outra forma sensata de fazer isso".
Se você quiser usar uma biblioteca existente, você poderia conferir o fp-ts. Caso contrário, um esboço de implementação poderia ser assim:
interface TypeFunction<T> { }
type TypeFunctionName = keyof TypeFunction<any>
type Apply<F extends TypeFunctionName, T> = TypeFunction<T>[F];
Para cada tipo de função sobre o qual você quer falar, você precisa mesclar uma entrada em TypeFunction<T>. Por exemplo:
interface TypeFunction<T> {
Array: Array<T>
}
e
interface TypeFunction<T> {
Foo: Foo<T>
}
Uma vez que você fizer isso, você pode mudar qualquer uso de M<T> para Apply<M, T>, onde M agora é a chave que você deu para a função de tipo em TypeFunction<T>. Então você pode dizer Apply<"Array", string> e será string[], ou Apply<"Foo", string> e será {x: string}.
Dado um esboço assim, podemos escrever Bind e Unit como
type Bind<M extends TypeFunctionName> =
<A>(ma: Apply<M, A>) => <B>(a_mb: ((a: A) => Apply<M, B>)) => Apply<M, B>;
type Unit<M extends TypeFunctionName> =
<A>(a: A) => Apply<M, A>;
(observe como o escopo de A e B mudou... você quer que um único Bind<"Array"> funcione para todos A e B) e até mesmo definir outras operações como fmap:
const fmap = <M extends TypeFunctionName>(
bind: Bind<M>, unit: Unit<M>) => <A, B>(f: (a: A) => B) =>
(ma: Apply<M, A>) => bind(ma)(a => unit(f(a)))
E só por diversão podemos implementar bind e unit para os monads Array e Foo:
namespace ArrayMonad {
const bind: Bind<"Array"> = ma => mf => ma.flatMap(mf);
const unit: Unit<"Array"> = a => [a];
const map = fmap(bind, unit);
const mapYell = map((x: number) => x + "!");
const strs = mapYell([1, 2, 3]);
console.log(strs) // ["1!", "2!", "3!"]
}
namespace FooMonad {
const bind: Bind<"Foo"> = ma => mf => mf(ma.x);
const unit: Unit<"Foo"> = a => ({ x: a });
const map = fmap(bind, unit);
const mapYell = map((x: number) => x + "!");
const fooStr = mapYell({ x: 1 });
console.log(fooStr) // {x: "1!"}
}
Parece bom, mais ou menos. Não estou muito satisfeito em ter que escrever Bind<"Foo"> em vez de Bind<Foo>, mas como não há Foo para se referir, isso é o mais próximo que você consegue atualmente.
Vou deixar a implementação de Maybe para você.
Link do playground para o código
