Programación funcional en TypeScript: polimorfismo de género de orden superior

¡Hola, Habr! Nombre del menú es Yuri Bogomolov, y que (probablemente) me puede saber con mi trabajo en una serie de #MonadicMondays twitteado sobre el canal en yutyube o artículos a mediano o dev.to . En el segmento de Internet de habla rusa, hay muy poca información sobre programación funcional en TypeScript y uno de los mejores ecosistemas para este lenguaje: la biblioteca fp-ts , al ecosistema al que contribuí de manera bastante activa hace algún tiempo. Con este artículo, quiero comenzar una historia sobre FP en TypeScript, y si hay una respuesta positiva de la comunidad Habra, continuaré la serie.



No creo que sea una revelación para nadie que TypeScript es uno de los superconjuntos de JS fuertemente tipados más populares. Después de habilitar el modo de compilación estricto y configurar el linter para prohibir el uso, anyeste lenguaje se vuelve adecuado para el desarrollo industrial en muchas áreas, desde CMS hasta software bancario y de corretaje. Para el sistema de tipos TypeScript, incluso ha habido intentos no oficiales de demostrar la integridad de Turing, lo que permite aplicar técnicas avanzadas de programación a nivel de tipo para garantizar la corrección de la lógica empresarial al hacer que los estados ilegales no sean representables.



Todo lo anterior dio impulso a la creación de una maravillosa biblioteca para programación funcional para TypeScript, fp-tspor parte del matemático italiano Giulio Canti. Una de las primeras cosas que encuentra una persona que quiere dominarlo son las definiciones muy específicas de los tipos de especies Kind<URI, SomeType>o interface SomeKind<F extends URIS> {}. En este artículo, quiero llevar al lector a comprender todas estas "complejidades" y mostrar que, de hecho, todo es muy simple y comprensible, solo tiene que comenzar a resolver este rompecabezas.



Parto de orden superior



Cuando se trata de programación funcional, los desarrolladores de JS generalmente se detienen en componer funciones puras y escribir combinadores simples. Pocos miran el territorio de la óptica funcional, y es casi imposible encontrarse coqueteando con APIs libres monádicas o esquemas de recursividad. De hecho, todas estas construcciones no son demasiado complejas y el sistema de tipos facilita enormemente el aprendizaje y la comprensión. TypeScript como lenguaje tiene capacidades expresivas bastante ricas, sin embargo, tienen su propio límite, lo cual es inconveniente: la ausencia de géneros / tipos / tipos. Para que quede más claro, veamos un ejemplo.



. , , — , : 0 N A. , A -> B, «» .map(), B, , :



const as = [1, 2, 3, 4, 5, 6]; // as :: number[]
const f = (a: number): string => a.toString();

const bs = as.map(f); // bs :: string[]
console.log(bs); // => [ '1', '2', '3', '4', '5', '6' ]


. map . , :



interface MappableArray {
  readonly map: <A, B>(f: (a: A) => B) => (as: A[]) => B[];
}


. , , map (Set), - (Map), , , … , . , map :



type MapForSet   = <A, B>(f: (a: A) => B) => (as: Set<A>) => Set<B>;
type MapForMap   = <A, B>(f: (a: A) => B) => (as: Map<string, A>) => Map<string, B>;
type MapForTree  = <A, B>(f: (a: A) => B) => (as: Tree<A>) => Tree<B>;
type MapForStack = <A, B>(f: (a: A) => B) => (as: Stack<A>) => Stack<B>;


- Map , , , .



, : Mappable. , , . TypeScript, , - -:



interface Mappable<F> {
  // Type 'F' is not generic. ts(2315)
  readonly map: <A, B>(f: (a: A) => B) => (as: F<A>) => F<B>;
}


, , TypeScript , - F . Scala F<_> - — . , ? , « ».





, TypeScript , , «» — . — , . (pattern-matching) . , , «Definitional interpreters for higher-order programming languages», , .



, : - Mappable, - F, , , - . , :



  1. - F — , , : 'Array', 'Promise', 'Set', 'Tree' .
  2. - Kind<IdF, A>, F A: Kind<'F', A> ~ F<A>.
  3. Kind -, — .


, :



interface URItoKind<A> {
  'Array': Array<A>;
} //    1-: Array, Set, Tree, Promise, Maybe, Task...
interface URItoKind2<A, B> {
  'Map': Map<A, B>;
} //    2-: Map, Either, Bifunctor...

type URIS = keyof URItoKind<unknown>; // -  «»  1-
type URIS2 = keyof URItoKind2<unknown, unknown>; //   2-
//   ,   

type Kind<F extends URIS, A> = URItoKind<A>[F];
type Kind2<F extends URIS2, A, B> = URItoKind2<A, B>[F];
//   


: URItoKindN , , . TypeScript, (module augmentation). , :



type Tree<A> = ...

declare module 'my-lib/path/to/uri-dictionaries' {
  interface URItoKind<A> {
    'Tree': Tree<A>;
  }
}

type Test1 = Kind<'Tree', string> //     Tree<string>


Mappable



Mappable - — 1- , :



interface Mappable<F extends URIS> {
  readonly map: <A, B>(f: (a: A) => B) => (as: Kind<F, A>) => Kind<F, B>;
}

const mappableArray: Mappable<'Array'> = {
  //  `as`    A[],  -    `Kind`:
  map: f => as => as.map(f)
};
const mappableSet: Mappable<'Set'> = {
  //   —   ,     ,
  //         ,   
  map: f => as => new Set(Array.from(as).map(f))
};
//   ,  Tree —      :   ,
//    ,     :
const mappableTree: Mappable<'Tree'> = {
  map: f => as => {
    switch (true) {
      case as.tag === 'Leaf': return f(as.value);
      case as.tag === 'Node': return node(as.children.map(mappableTree.map(f)));
    }
  }
};


, Mappable , Functor. T fmap, A => B T<A> T<B>. , A => B T ( , Reader/Writer/State).



fp-ts



, fp-ts. , : https://gcanti.github.io/fp-ts/guides/HKT.html. — fp-ts URItoKind/URItoKind2/URItoKind3, fp-ts/lib/HKT.



fp-ts :





:








. , , , . , , . , , Mappable/Chainable .., — , , ? , .




All Articles