Fp-ts: Question: TaskEither and Option

Created on 28 Jan 2020  路  3Comments  路  Source: gcanti/fp-ts

I'm starting with fp-ts and am trying to find a solution to combine in an elegant way both async tasks and possible Option.

Considering the following code:

import * as O from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'
import * as TE from 'fp-ts/lib/TaskEither'
import * as T from 'fp-ts/lib/Task'
import * as E from 'fp-ts/lib/Either'


async function main() {
  const myjob = pipe(
    TE.right(new Date('2020-01-14')),
    TE.chain(d => TE.tryCatch(() => MonthExchangeRate('USD', 'EUR', d), E.toError)),
    TE.chain(x => TE.tryCatch(() => add2(x), E.toError)),
    TE.chain(x => TE.tryCatch(() => someErr(x), E.toError)),
    TE.chain(x => TE.tryCatch(() => genSomeErr(x, true), E.toError))
  )

  const result = await myjob()

  pipe(
    result,
    E.fold(
      e => console.log('fold =', e),
      res => console.log('fold =', res)
    )
  )

// Alternative
  if (E.isLeft(result)) {
    console.log('Erreur')
    console.log(result.left)
  } else {
    console.log('Tout va bien')
    console.log(result.right)
  }

  console.log('** Raw Result **')
  console.log(result)
}

;(async () => main())()

If i'm not returning an Option from any TaskEither, everything works fine.
Now, I would like MonthExchangeRate to return a Promise And also, I would like myJob to stop the pipe with a specific error in that case.

In pseudo language it would be something like:

  const myjob = pipe(
    TE.right(new Date('2020-01-14')),
    TE.chain(d => TE.tryCatch(() => MonthExchangeRate('USD', 'EUR', d), E.toError)),

   //== PSEUDO Start
    TE.chain( y => IF y is none ALORS Left(new Error('no rate found')) // pipe is stopped
    //== PSEUDO End

    TE.chain(x => TE.tryCatch(() => add2(x), E.toError)),
    TE.chain(x => TE.tryCatch(() => someErr(x), E.toError)),
    TE.chain(x => TE.tryCatch(() => genSomeErr(x, true), E.toError))
  )
  const result = await myjob()

// Alternative
  if (E.isLeft(result)) {
    console.log('Erreur')
    console.log(result.left)
  } else {
    console.log('Tout va bien')
    console.log(result.right)
  }

// When MonthExchangeRate resulte is Promise<none> because no rate was found
// it should display 
//  Erreur 
// No rate found + stack trace

Hopefully, I'm clear enough.
I guess it must be trivial for somewhat knowledgeable. I tried various things but I'm having a hard time to make complete sense of all this.
I like the benefits of the approach however I have to admit (and accept) that a big part of it is magical at the moment.

Thanks in advance for any help.

Most helpful comment

You want to go from

function originalMonthExchangeRate(_from: string, _to: string, _date: Date): Promise<number> {
  return Promise.resolve(1)
}

function monthExchangeRate(from: string, to: string, date: Date): TE.TaskEither<Error, number> {
  return TE.tryCatch(() => originalMonthExchangeRate(from, to, date), E.toError)
}

to

function originalMonthExchangeRate(_from: string, _to: string, _date: Date): Promise<O.Option<number>> {
  return Promise.resolve(O.none)
}

function monthExchangeRate(from: string, to: string, date: Date): TE.TaskEither<Error, number> {
  return pipe(
    TE.tryCatch(() => originalMonthExchangeRate(from, to, date), E.toError),
    T.map(E.chain(E.fromOption(() => new Error('no rate found'))))
  )
}

Whole program

import * as E from 'fp-ts/lib/Either'
import * as O from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'
import * as T from 'fp-ts/lib/Task'
import * as TE from 'fp-ts/lib/TaskEither'

function originalMonthExchangeRate(_from: string, _to: string, _date: Date): Promise<O.Option<number>> {
  return Promise.resolve(O.none)
}

function monthExchangeRate(from: string, to: string, date: Date): TE.TaskEither<Error, number> {
  return pipe(
    TE.tryCatch(() => originalMonthExchangeRate(from, to, date), E.toError),
    T.map(E.chain(E.fromOption(() => new Error('no rate found'))))
  )
}

const main: T.Task<void> = pipe(
  TE.right(new Date('2020-01-14')),
  TE.chain(d => monthExchangeRate('USD', 'EUR', d)),
  // ... more piping ...
  TE.fold(
    e =>
      T.fromIO(() => {
        console.log('Erreur')
        console.log(e)
      }),
    res =>
      T.fromIO(() => {
        console.log('Tout va bien')
        console.log(res)
      })
  )
)

main()
/*
Erreur
Error: no rate found
... stack trace ...
*/

All 3 comments

Just take a look at the definition of TaskEither and you willl see that it is composed out of Task and Either so you are able to use the Functor Instance of Task and have direct access to the Either instance. This would look like this

import * as T from 'fp-ts/lib/Task'
import * as E from 'fp-ts/lib/Either'

const myjob = pipe(
  TE.right(new Date('2020-01-14')),
  TE.chain(d => TE.tryCatch(() => MonthExchangeRate('USD', 'EUR', d), E.toError)),

  T.map(E.chain(E.fromOption(() => new Error('no rate found)))),

  TE.chain(x => TE.tryCatch(() => add2(x), E.toError)),
  TE.chain(x => TE.tryCatch(() => someErr(x), E.toError)),
  TE.chain(x => TE.tryCatch(() => genSomeErr(x, true), E.toError))
)

You want to go from

function originalMonthExchangeRate(_from: string, _to: string, _date: Date): Promise<number> {
  return Promise.resolve(1)
}

function monthExchangeRate(from: string, to: string, date: Date): TE.TaskEither<Error, number> {
  return TE.tryCatch(() => originalMonthExchangeRate(from, to, date), E.toError)
}

to

function originalMonthExchangeRate(_from: string, _to: string, _date: Date): Promise<O.Option<number>> {
  return Promise.resolve(O.none)
}

function monthExchangeRate(from: string, to: string, date: Date): TE.TaskEither<Error, number> {
  return pipe(
    TE.tryCatch(() => originalMonthExchangeRate(from, to, date), E.toError),
    T.map(E.chain(E.fromOption(() => new Error('no rate found'))))
  )
}

Whole program

import * as E from 'fp-ts/lib/Either'
import * as O from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'
import * as T from 'fp-ts/lib/Task'
import * as TE from 'fp-ts/lib/TaskEither'

function originalMonthExchangeRate(_from: string, _to: string, _date: Date): Promise<O.Option<number>> {
  return Promise.resolve(O.none)
}

function monthExchangeRate(from: string, to: string, date: Date): TE.TaskEither<Error, number> {
  return pipe(
    TE.tryCatch(() => originalMonthExchangeRate(from, to, date), E.toError),
    T.map(E.chain(E.fromOption(() => new Error('no rate found'))))
  )
}

const main: T.Task<void> = pipe(
  TE.right(new Date('2020-01-14')),
  TE.chain(d => monthExchangeRate('USD', 'EUR', d)),
  // ... more piping ...
  TE.fold(
    e =>
      T.fromIO(() => {
        console.log('Erreur')
        console.log(e)
      }),
    res =>
      T.fromIO(() => {
        console.log('Tout va bien')
        console.log(res)
      })
  )
)

main()
/*
Erreur
Error: no rate found
... stack trace ...
*/

@gcanti , @mlegenhausen
Thanks a lot for the prompt and detailed answer.
I was not sure to be clear but you got it perfectly.
Also, I was not able to fold within the pipeline : the missing piece was T.fromIO. Thanks for the clue ;)

Now, honestly, I don't get exactly what's going on ... too many nested generics for my brain. I need to acquire a better understanding of the core concepts to decode and make sense of those somewhat intricate generic definitions. That said it sounds quite interesting. I prepared a couple of books and articles for my coming week-ends.

In the meantime, I believe that I can still use Options, TaskEither and Pipelines to improve the overall quality of my code.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Crashthatch picture Crashthatch  路  4Comments

mustafaekim picture mustafaekim  路  4Comments

mohsensaremi picture mohsensaremi  路  3Comments

maciejsikora picture maciejsikora  路  3Comments

mmkal picture mmkal  路  3Comments