Programação funcional em TypeScript: polimorfismo de gênero de ordem superior

Olá, Habr! Nome do menu é Yuri Bogomolov, e você (provavelmente) pode me conhecer pelo meu trabalho em uma série de #MonadicMondays twittou sobre o canal no yutyube ou artigos para Médio ou dev.to . No segmento de língua russa da Internet, há muito pouca informação sobre programação funcional em TypeScript e um dos melhores ecossistemas para essa linguagem - a biblioteca fp-ts , para o ecossistema com o qual contribuí ativamente há algum tempo. Com este artigo, quero começar uma história sobre FP no TypeScript e, se houver uma resposta positiva da comunidade Habra, continuarei a série.



Não acho que será uma revelação para ninguém que o TypeScript é um dos superconjuntos mais populares de JS com tipagem forte. Depois de habilitar o modo de compilação estrito e definir o linter para proibir o uso, anyessa linguagem se torna adequada para desenvolvimento industrial em muitas áreas - de CMS a software bancário e de corretagem. Para o sistema de tipo TypeScript, houve até tentativas não oficiais de provar a integridade de Turing, o que permite que técnicas de programação de nível de tipo avançadas sejam aplicadas para garantir a correção da lógica de negócios, tornando os estados ilegais irrepresentáveis.



Todos os itens acima deram impulso à criação de uma biblioteca maravilhosa para programação funcional para TypeScript - de fp-tsautoria do matemático italiano Giulio Canti. Uma das primeiras coisas que uma pessoa que deseja dominar encontra são as definições muito específicas dos tipos da espécie Kind<URI, SomeType>ou interface SomeKind<F extends URIS> {}. Neste artigo, quero levar o leitor a compreender todas essas "complexidades" e mostrar que na verdade tudo é muito simples e claro - você só precisa começar a desvendar esse quebra-cabeça.



Parto de ordem superior



Quando se trata de programação funcional, os desenvolvedores JS geralmente param em compor funções puras e escrever combinadores simples. Poucos olham para o território da óptica funcional e é quase impossível encontrar flertes com APIs freemonadic ou esquemas de recursão. Na verdade, todas essas construções não são excessivamente complicadas e o sistema de tipos facilita muito o aprendizado e a compreensão. O TypeScript como linguagem tem recursos expressivos bastante ricos, entretanto, eles têm seu limite, o que é inconveniente - a ausência de gêneros / cinds / tipos. Para tornar isso mais claro, vejamos um exemplo.



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