Functional Programming in TypeScript: The Typeclass Pattern

Previous articles in the series:







  1. Higher order genus polymorphism





In the previous article, I described how you can emulate higher-order genus polymorphism in TypeScript. Let's now take a look at what this gives the functional programmer, and we'll start with the type class pattern.







The concept of a typeclass itself came from Haskell and was first proposed by Philip Wadler and Stephen Blott in 1988 to implement ad hoc polymorphism. A type class defines a set of typed functions and constants that must exist for each type that belongs to a given class. It sounds complicated at first, but it's actually quite a simple and elegant design.







What is a type class



Immediately a disclaimer for those who are well versed in Haskell or Scala

, , . - TypeScript JavaScript , ( this



). , , GHC Core Language, .







Consider, as an example, one of the simplest type classes - Show



, - that defines a casting operation. It is defined in the module fp-ts/lib/Show



:







interface Show<A> {
  readonly show: (a: A) => string;
}
      
      





This definition reads like this: a type A



belongs to a class Show



if A



a function is defined forshow : (a: A) => string



.







The type class is implemented as follows:







const showString: Show<string> = {
  show: s => JSON.stringify(s)
};

const showNumber: Show<number> = {
  show: n => n.toString()
};

// ,    «»   name  age:
const showUser: Show<User> = {
  show: user => `User "${user.name}", ${user.age} years old`
};
      
      





. , Show



β€” , , β€” Show



:







//     any , ..   T  
//   Show β€”       infer.
//      T  ,   
//  Show:
const getShowTuple = <T extends Array<Show<any>>>(
  ...shows: T
): Show<{ [K in keyof T]: T[K] extends Show<infer A> ? A : never }> => ({
  show: t => `[${t.map((a, i) => shows[i].show(a)).join(', ')}]`
});
      
      





(principle of least knowledge, principle of least power) β€” , . TypeScript , .







β€” , . , , . Mappable, Functor β€” . β€” , , map



, ; β€” map



; - β€” map



. :







import { Kind } from 'fp-ts/lib/HKT';
import { Functor } from 'fp-ts/lib/Functor';
import { Show } from 'fp-ts/lib/Show';

const stringify = <F extends URIS, A>(F: Functor<F>, A: Show<A>) =>
  (structure: Kind<F, A>): Kind<F, string> => F.map(structure, A.show);
      
      





, Β« Β» , ? β€” , .







, . , , , β€” :







interface Comment {
  readonly author: string;
  readonly text: string;
  readonly createdAt: Date;
}

const comments: Comment[] = ...;

const renderComments = (comments: Comment[]): Component => <List>{comments.map(renderOneComment)}</List>;
const renderOneComment = (comment: Comment): Component => <ListItem>{comment.text} by {comment.author} at {comment.createdAt}</ListItem>
      
      





, , , , comments



.







, :







interface ToComponent<A> {
  readonly render: (element: A) => Component;
}

const commentToComponent: ToComponent<Comment> = {
  render: comment => <>{comment.text} by {comment.author} at {comment.createdAt}</>
};

const arrayToComponent = <A>(TCA: ToComponent<A>): ToComponent<Comment[]> => ({
  render: as => <List>{as.map(a => <ListItem>{TCA.render(a)}</ListItem>)}</List>
});

const treeToComponent = <A>(TCA: ToComponent<A>): ToComponent<Tree<Comment>> => ({
  render: treeA => <div class="node">
    {TCA.render(treeA.value)}
    <div class="inset-relative-to-parent">
      {treeA.children.map(treeToComponent(TCA).render)}
    </div>
  </div>
});

const renderComments = 
  <F extends URIS>(TCF: ToComponent<Kind<F, Comment>>) => 
    (comments: Kind<F, Comment>) => TCF.render(comments);

...

// -       :
const commentArray: Comment[] = getFlatComments();
renderComments(arrayToComponent(commentToComponent))(commentArray);
// ... ,     :
const commentTree: Tree<Comment> = getCommentHierarchy();
renderComments(treeToComponent(commentToComponent))(commentTree);
      
      





, TypeScript :







  1. , , , .
  2. , , «» . , /instance β€” .
  3. UPPER_SNAKE_CASE, camelCase . , , β€” $tyled_like_php, .




fp-ts



, , «» .







Functor (fp-ts/lib/Functor)



map : <A, B>(f: (a: A) => B) => (fa: F<A>) => F<B>



, :







  1. - F



    , A => B



    F<A>



    , F<B>



    .
  2. A => B



    F



    , F<A> => F<B>



    .


, , , , -. , β€” - .







:







  1. : map(id) ≑ id



  2. : map(compose(f, g)) ≑ compose(map(f), map(g))





, β€” , , , , , Functor map



.







Monad (fp-ts/lib/Monad)



, , , railway, . , . , !







Β«1-2-3Β»: 1 , 2 3 :







  1. β€” , Array, List, Tree, Option, Reader .. β€” , , .
  2. , β€” chain



    join



    , of



    :

    1. :

      of : <A>(value: A) => F<A>
      chain : <A, B>(f: (a: A) => F<B>) => (fa: F<A>) => F<B>
            
            



    2. :

      of : <A>(value: A) => F<A>
      join : <A>(ffa: F<F<A>>) => F<A>
            
            



  3. , :

    1. : chain(f)(of(a)) ≑ f(a)



    2. : chain(of)(m) ≑ m



    3. : chain(g)(chain(f)(m)) ≑ chain(x => chain(g)(f(x)))(m)





of



pure



, chain



>>=



( Β«bindΒ»):







  1. : pure a >>= f ≑ f a



  2. : m >>= pure ≑ m



  3. : (m >>= f) >>= g ≑ m >>= (\x -> f x >>= g)





, , , . : type Reader<R, A> = (env: R) => A



.







, , , . , β€” , , . , (property-based testing).







. chain



: «» F



A



, β€” , A



, B



. A



F<A>



, F



. β€” Promise<A>



, A



«» . , , .







- β€” do-, for comprehension, β€” TS . - , Do fp-ts-contrib. .







Monoid (fp-ts/lib/Monoid)



:







  1. , /unit: empty : A



  2. : combine : (left: A, right: A) => A





3 :







  1. : combine(empty, x) ≑ x



  2. : combine(x, empty) ≑ x



  3. : combine(combine(x, y), z) ≑ combine(x, combine(y, z))





? β€” , , . , Β«Monoids, monoids, monoidsΒ». Scala, β€” .










β€” , Foldable/Traversable , - ; Applicative ( , ) ; Task/TaskEither/Future , . . , .








All Articles