简体   繁体   中英

How to manage monads in FP and specially in fp-ts

I'm very new to functional programming and specially fp-ts library.

My question includes two parts:

  1. I'm seeing a pattern of turning Monads from one type to another like from Task to IO or vice versa, how do we manage this, should we stay always on one or should we change as the chain continues?
  2. How to simply make typescript follow of these type changes going from one to another?

For example let's say we have a couple of functions and we wanna compose them together as below, I know that probably this example is not very practical but it serves the purpose.

declare function getRnd(min: number, max: number): IO<number>; // Returns a random number within the range
declare function getPage(pageNo: number): TaskEither<Error, string>; // Make an Http request
declare function getLinks(pageContent: string): Option<string[]>; // Returns some links

// Let's say we wanna get a random page number and then return the links on it
// How do we compose these functions?
const getPageLinks = pipe(
  getRnd(2, 4),
  IO.chain(getPage), // I'm pretty sure TS will yells at me here
  TaskEither.chain(getLinks),
  log, // ?
)

1.) turning Monads from one type to another, how do we manage this, should we stay always on one or should we change as the chain continues?

You want some kind of (natural) transformation to switch from IO to Task / TaskEither . The other way round doesn't make sense to me, as an async effect cannot be converted to a sync one.

chain will preserve the structure . So getPage in IO.chain(getPage) needs a signature number -> IO<whatever> . You can instead use map to add an additional layer of nesting, like:

pipe(getRnd(2, 4), IO.map(getPage)); // I.IO<TE.TaskEither<Error, string>>

In general, there is no right or wrong way, it just depends on the purpose. Note, the more nested data types, the more complex it will become to process the inner value(s). Part of functional programming with algebraic structures is to avoid unnecessary nesting right at the source.

In your case it does indeed make sense to consolidate everything in a uniform TaskEither - you won't have any advantage with a type IO<TaskEither<...>> vs TaskEither<...> .

2.) How to simply make typescript follow of these type changes going from one to another?

You can use TaskEither.rightIO to transform IO to TaskEither ( CodeSandbox ):

import { taskEither as TE, io as I, option as O, pipeable as P, either as E } from "fp-ts";

declare function log<T>(t: T): I.IO<T>;

const getPageLinks = P.pipe(
  getRnd(2, 4),
  TE.rightIO,
  TE.chain(getPage),
  TE.map(getLinks),
  TE.chain(v => TE.rightIO(log(v)))
); // TE.TaskEither<Error, O.Option<string[]>>

This one also works, as TS uses structural typing (but I would recommend former):

const getPageLinks2 = P.pipe(
  getRnd(2, 4),
  I.chain(getPage), // this also works
  I.map(v => v.then(vv => E.either.map(vv, getLinks))),
  I.chain(log)
); // I.IO<Promise<E.Either<Error, O.Option<string[]>>>>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM