Búsqueda de sitios web

programación funcional - Parámetro de tipo genérico y mónadas en TypeScript


Actualmente estoy aprendiendo sobre mónadas y tratando de usar TypeScript para entenderlas mejor.

Me gustaría hacer lo siguiente:

type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
type Unit<M, A> = (a: A) => M<A>

Pero estoy recibiendo los siguientes errores de 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 una manera de especificar que el parámetro de tipo M debería ser genérico? ¿O hay otra forma lógica de hacer esto?

La idea es que más tarde puedo pasar, por ejemplo, Maybe en lugar de M, donde Maybe es un tipo genérico:

type Maybe<T> = { kind: 'nothing' } | { kind: 'some', value: T }

Respuesta #1

No, TypeScript no soporta directamente tipos de orden superior del tipo necesario para definir Bind o Unit. Hay una solicitud de característica abierta desde hace mucho tiempo en microsoft/TypeScript#1213 que lo pide, y no está claro cuándo o si alguna vez será implementada.

TypeScript actualmente carece de la capacidad de abstraer sobre funciones de tipo. (Una función de tipo actúa sobre tipos de la misma manera que una función regular actúa sobre valores; toma algunos tipos de entrada y produce un tipo de salida). Puede representar funciones de tipo concretas individuales como Array, y puedes definir nuevas funciones de tipo concretas individuales como:

type Foo<T> = {x: T} // Foo is a type function

pero no puedes hablar sobre funciones de tipo en general (no existe un Foo por sí mismo para referenciar), ni puedes pasar funciones de tipo a otras funciones de tipo de orden superior. Dado que tu parámetro de tipo M necesita ser una función de tipo arbitraria para que esto funcione, no hay manera de hacerlo directamente.


Nota que dije que no soporta directamente tipos de orden superior. Es posible emular tipos de orden superior, pero todas las formas de hacerlo que conozco son torpes y necesitan mucha intervención manual y código repetitivo. No sé si lo llamaría "loco", pero tampoco diría necesariamente que es "otra forma sensata de hacer esto".

Si quieres usar una biblioteca existente, podrías revisar fp-ts. De lo contrario, un bosquejo de una implementación podría verse así:

interface TypeFunction<T> { }
type TypeFunctionName = keyof TypeFunction<any>
type Apply<F extends TypeFunctionName, T> = TypeFunction<T>[F];

Para cada tipo de función del que quieras hablar, necesitas combinar una entrada en TypeFunction<T>. Por ejemplo:

interface TypeFunction<T> {
  Array: Array<T>
}

y

interface TypeFunction<T> {
  Foo: Foo<T>
}

Una vez que hagas eso, puedes cambiar cualquier uso de M<T> a Apply<M, T> donde M es ahora la clave que diste a la función de tipo en TypeFunction<T>. Así que puedes decir Apply<"Array", string> y será string[], o Apply<"Foo", string> y será {x: string}.


Dado un esquema así, podemos escribir Bind y 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>;

(observa cómo ha cambiado el alcance de A y B... quieres que un solo Bind<"Array"> funcione para todos A y B) e incluso definir otras operaciones 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)))

Y solo por diversión podemos implementar bind y unit para los mónadas Array y 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!"}
}

Se ve bien, más o menos. No me entusiasma tener que escribir Bind<"Foo"> en lugar de Bind<Foo>, pero dado que no hay un Foo al que referirse, esto es lo más cercano que se puede conseguir actualmente.

Dejaré la implementación de Maybe a ti.

Enlace de Playground al código