TypeScriptの機能プログラミング:高次の性別多型

こんにちは、Habr!メニュー名はYuriBogomolovです。あなたは(おそらく)yutyubeのチャンネルでツイートされた一連の#MonadicMondays、Mediumまたはdev.toへの記事での私の仕事について私を知ることができます。インターネットのロシア語圏では、TypeScriptの機能プログラミングに関する情報はほとんどなく、この言語に最適なエコシステムの1つであるfp-tsライブラリは、私が以前に積極的に貢献したエコシステムです。この記事では、TypeScriptでFPについての話を始めたいと思います。そして、Habraコミュニティから肯定的な反応があれば、シリーズを続けます。



TypeScriptがJSの最も人気のある強く型付けされたスーパーセットの1つであることは、誰にとっても明らかなことではないと思います。厳密なコンパイルモードを有効にし、リンターを使用を禁止するように設定すると、anyこの言語は、CMSから銀行および仲介ソフトウェアまで、多くの分野での産業開発に適したものになります。 TypeScriptタイプのシステムでは、Turingの完全性を証明する非公式の試みもありました。これにより、高度なタイプレベルのプログラミング手法を適用して、違法な状態を表現できないようにすることでビジネスロジックの正確性を確保できます。



上記のすべてが、fp-tsイタリアの数学者GiulioCantiによるTypeScriptの機能プログラミング用の素晴らしいライブラリの作成に弾みをつけましたそれをマスターしたい人が遭遇したことを最初のものの一つは、種の種類の非常に具体的な定義ですKind<URI, SomeType>interface SomeKind<F extends URIS> {}この記事では、読者にこれらすべての「複雑さ」を理解させ、実際にはすべてが非常に単純で理解しやすいことを示したいと思います。このパズルを解き始める必要があります。



高次の出産



機能プログラミングに関しては、JS開発者は通常純粋な関数の作成と単純なコンビネーターの作成にとどまります。機能光学系の領域を調べることはほとんどなく、フリーモナディックAPIや再帰スキームでいじくり回すことはほとんど不可能です。実際、これらすべての構造はそれほど複雑ではなく、タイプシステムは学習と理解を非常に容易にします。言語としてのTypeScriptには非常に豊富な表現機能がありますが、制限があり、不便です。性別、種類、種類がないためです。明確にするために、例を見てみましょう。



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