1

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, // ?
)
Amin Paks
  • 276
  • 2
  • 15
  • You can't just compose from `IO` to `TaskEither`. You need a mapping of the involved value constructors. This is called a natural transformation and there are laws to take into account. –  Mar 18 '20 at 08:57
  • Sure, you mean we need to use a constructor to change the effect going from one to another. But do I have to rewrite all my functions to return the same effect all the time? @bob – Amin Paks Mar 18 '20 at 11:02

1 Answers1

2

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[]>>>>
ford04
  • 66,267
  • 20
  • 199
  • 171
  • `I.IO>` could be probably composed using a monad transformer. However, what I still don't fully understand is the delimitation from natural transformations and monad transformers. When do I want to lawfully copy values from one structure to another and when do I want to compose nested structures with transformers? –  Mar 19 '20 at 07:49