Programmation fonctionnelle dans TypeScript: polymorphisme de genre d'ordre supérieur

Bonjour, Habr! Le nom du menu est Yuri Bogomolov, et vous (probablement) peut me connaßtre pour mon travail sur une série de #MonadicMondays tweeté sur le canal sur yutyube ou des articles à moyen ou dev.to . Dans le segment russophone d'Internet, il y a trÚs peu d'informations sur la programmation fonctionnelle en TypeScript et l'un des meilleurs écosystÚmes pour ce langage - la bibliothÚque fp-ts , à l'écosystÚme duquel j'ai contribué assez activement il y a quelque temps. Avec cet article, je veux commencer une histoire sur FP dans TypeScript, et s'il y a une réponse positive de la communauté Habra, je continuerai la série.



Je ne pense pas que ce sera une rĂ©vĂ©lation Ă  qui que ce soit que TypeScript est l'un des sur-ensembles fortement typĂ©s les plus populaires de JS. AprĂšs avoir activĂ© le mode de compilation strict et paramĂ©trĂ© le linter pour interdire l'utilisation, anyce langage devient adaptĂ© au dĂ©veloppement industriel dans de nombreux domaines - du CMS aux logiciels bancaires et de courtage. Pour le systĂšme de type TypeScript, il y a mĂȘme eu des tentatives non officielles pour prouver l'exhaustivitĂ© de Turing, ce qui permet d'appliquer des techniques avancĂ©es de programmation au niveau du type pour garantir l'exactitude de la logique mĂ©tier en rendant les Ă©tats illĂ©gaux irreprĂ©sentables.



Tout ce qui précÚde a donné une impulsion à la création d'une merveilleuse bibliothÚque de programmation fonctionnelle pour TypeScript - fp-tspar le mathématicien italien Giulio Canti. Une des premiÚres choses que rencontre une personne qui veut la maßtriser, ce sont les définitions trÚs spécifiques des types d'espÚces Kind<URI, SomeType>ou interface SomeKind<F extends URIS> {}. Dans cet article, je veux amener le lecteur à comprendre toutes ces «complexités» et montrer qu'en fait tout est trÚs simple et clair - il vous suffit de commencer à dérouler ce puzzle.



Accouchement d'un ordre supérieur



En ce qui concerne la programmation fonctionnelle, les dĂ©veloppeurs JS s'arrĂȘtent gĂ©nĂ©ralement Ă  la composition de fonctions pures et Ă  l'Ă©criture de simples combinateurs. Rares sont ceux qui se penchent sur le territoire de l'optique fonctionnelle, et il est presque impossible de tomber sur le flirt avec des API freemonadic ou des schĂ©mas de rĂ©cursivitĂ©. En fait, toutes ces constructions ne sont pas trop compliquĂ©es et le systĂšme de types facilite grandement l'apprentissage et la comprĂ©hension. TypeScript en tant que langage a des capacitĂ©s expressives assez riches, cependant, ils ont leur limite, ce qui est gĂȘnant - l'absence de genres / cinds / genres. Regardons un exemple pour le rendre plus clair.



. , , — , : 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