4

I have the following program that works fine when none of the functions is async.

interface Product {
  count: number
  pricePerItem: number
}

interface Tax {
  tax: number
}

interface Delivery {
  delivery: number
}

interface PTD { //ProductTaxDelivery
  p: Product
  t: number
  d: number
}

function getProduct(): Either<Error, Product> {
  return E.right({ count: 10, pricePerItem: 5 })
}

function getTax(p: Product): Either<Error, number> {
  return E.right(p.pricePerItem * p.count * 0.085)
}

function getDelivery(p: Product): Either<Error, number> {
  return  E.right(p.count * 0.05)
  //or maybe return E.left(Error('some error in delivery happened'))
}
function run(): Either<Error, PTD> {
  return pipe(
    E.Do,
    E.bind('p', getProduct),
    E.bind('tax', ({p}) => getTax(p)),
    E.bind('delivery', ({p}) => getDelivery(p)),    
    E.map(({ p, tax, delivery }) => ({ p, t: tax, d: delivery }))
  )
}
function main() {
  pipe(
    run(),
    E.fold(
      (e) => {
        console.log(`error: ${e}`)
      },
      (it) => {
        console.log(`ok ${it.p.count} ${it.p.pricePerItem} ${it.t} ${it.d}`)
      }
    )
  )
}

main()

The question I'm having is if one of my functions, for example getDelivery() is async, then I'm not sure how to solve it.

Here's what I have tried:

function getDelivery(p: Product): TaskEither<Error, number> {
  return TE.right(p.count * 0.05)
}

TE.bind('delivery', ({p}) => getDelivery(p)),

and many other variations, but all ended up in compiler errors.

The equivalent in imperative style is something like:

const getDelivery = async (p: Product) => {
   return await something()
}

const run = async (): PTD => {
   const product = getProduct()
   const tax = getTax(product)
   const delivery = await getDelivery(product)
   
   return {
      p: product, t: tax, d: delivery
   }
}

What is the correct functional way (that I think involves both Either and TaskEither) using fp-ts?

Update: I also tried to replace Either with TaskEither, E with TE everywhere, but the problem is now a compiler error when I tried to fold in main(). Here's the code that replaces:

function getProduct(): TaskEither<Error, Product> {
  return TE.right({ count: 10, pricePerItem: 5 })
}

function getTax(p: Product): TaskEither<Error, number> {
  return TE.right(p.pricePerItem * p.count * 0.085)
}

function getDelivery(p: Product): TaskEither<Error, number> {
  return TE.right(p.count * 0.05)
}

function run(): TaskEither<Error, PTD> {
  return pipe(
    TE.Do,
    TE.bind('p', getProduct),
    TE.bind('tax', ({ p }) => getTax(p)),
    TE.bind('delivery', ({ p }) => getDelivery(p)),
    TE.map(({ p, tax, delivery }) => ({ p, t: tax, d: delivery }))
  )
}

function main() {
  pipe(
    run(),
    TE.fold(
      (e) => { 
        console.log(`error: ${e}`)
      },
      (it) => {
        console.log(`ok ${it.p.count} ${it.p.pricePerItem} ${it.t} ${it.d}`)
        //doNonFunctional()
      }
    )
  )
}

main()

On line with (e) => {, the compiler error says:

error TS2345: Argument of type '(e: Error) => void' is not assignable to parameter of type '(e: Error) => Task<unknown>'.
  Type 'void' is not assignable to type 'Task<unknown>'.

Update 2 OK, so I get the code to compile but no output when the program runs

const printError = (e: Error): T.Task<unknown> => {
  console.log(`error: ${e}`)
  return () => Promise.resolve()
}

const printPTD = (ptd: PTD): T.Task<unknown> => {
  console.log(`ok ${ptd.p.count} ${ptd.p.pricePerItem} ${ptd.t} ${ptd.d}`)
  return () => Promise.resolve()
}

function run(): TaskEither<Error, PTD> {
  return pipe(
    TE.Do,
    TE.bind('p', getProduct),
    TE.bind('tax', ({ p }) => getTax(p)),
    TE.bind('delivery', ({ p }) => getDelivery(p)),
    TE.map(({ p, tax, delivery }) => ({ p, t: tax, d: delivery }))
  )
}

function main() {
  pipe(
    run(),
    TE.fold(
      (e) => printError(e),
      (ptd) => printPTD(ptd)      
    )
  )
}

main()
Kevin Le - Khnle
  • 10,579
  • 11
  • 54
  • 80
  • First of all `Either` encodes short circuiting. If you use its monad instance with function composition you partially lose the effect. Besides you only need monad if the next computation depends on the previsous result. –  Oct 17 '21 at 11:57
  • 1
    Monads are systemic, i.e. if a function returns a monad, all other involved functions need to deal with this very monad as well. If you want to mix different monads you have to compose them using a monad transformer or you can implement a monad specific to the composed effects (e.g. `TaskEither`). So you need to lift all your sync functions into the context of `TaskEither`. –  Oct 17 '21 at 12:05
  • @IvenMarquardt I tried that too. I replaced Either with TaskEither, E with TE everywhere, but there's a compiler error. – Kevin Le - Khnle Oct 17 '21 at 12:35

1 Answers1

6

The issue is when you create a Task in main with pipe, you are not actually running anything.

This is how Task is defined:

interface Task<A> {
  (): Promise<A>
}

// same as type Task<A> = () => Promise<A>

Because Task is a thunk, you need to call it to actually execute the code.

async function main(): Promise<void> {
  await pipe(
    // ...
// vv note the call here
  )()
}

main()

However, I would do it like this:

const main: T.Task<void> = pipe(/* ... */)

main()

Similarly, run doesn't need to be a function; it can be const run = pipe(/* ... */).

Also, there's a Console module that provides log functions that return an IO (a type for side-effectful actions).

Your code could be written as

import * as Console from 'fp-ts/Console'
import * as E from 'fp-ts/Either'
import * as T from 'fp-ts/Task'
import * as TE from 'fp-ts/TaskEither'
import {pipe} from 'fp-ts/function'

// <A>(a: A) => Task<void>
const taskLog = T.fromIOK(Console.log)

// You can still keep getProduct and getTask synchronous
function getProduct(): E.Either<Error, Product> { /* ... */ }
function getTax(p: Product): E.Either<Error, number> { /* ... */ }

function getDelivery(p: Product): TE.TaskEither<Error, number> { /* ... */ }

const run: TE.TaskEither<Error, PTD> = pipe(
  TE.Do,
  // See below for what TE.fromEither(K) does
  TE.bind('p', TE.fromEitherK(getProduct)),
  TE.bind('tax', ({p}) => TE.fromEither(getTax(p))),
  TE.bind('delivery', ({p}) => getDelivery(p)),
  TE.map(({p, tax, delivery}) => ({p, t: tax, d: delivery}))
)

const main: T.Task<void> = pipe(
  run,
  TE.fold(
    e => taskLog(`error: ${e}`),
    it => taskLog(`ok ${it.p.count} ${it.p.pricePerItem} ${it.t} ${it.d}`)
  )
)

main().catch(console.error)

TE.fromEither converts an Either into a TaskEither:

export declare const fromEither: NaturalTransformation22<'Either', 'TaskEither'>
// same as
export declare const fromEither: <E, A>(fa: Either<E, A>) => TaskEither<E, A>

TE.fromEitherK is the same as fromEither but for functions:

export declare const fromEitherK: <E, A extends readonly unknown[], B>(f: (...a: A) => Either<E, B>) => (...a: A) => TaskEither<E, B>

You can probably guess by now what T.fromIOK (used for taskLog) does:

export declare const fromIOK: <A, B>(f: (...a: A) => IO<B>) => (...a: A) => Task<B>

Here's a CodeSandbox with the full code.

Lauren Yim
  • 12,700
  • 2
  • 32
  • 59
  • Wow, I have spent so much time searching everywhere, reading and trying everything. Where did you get all of that knowledge ? Thanks a lot anyway. – Kevin Le - Khnle Oct 19 '21 at 04:42
  • 1
    @KevinLe ‘Where did you get all of that knowledge’ the documentation is a good place to start :) – Lauren Yim Oct 19 '21 at 04:49
  • I've never found the documentation for fp-ts very helpful. – ironchicken Oct 21 '21 at 20:20
  • @ironchicken True, the documentation assumes you are familiar with functional programming already. I was referring to how to convert between `Task`s and `TaskEither`s, which are fp-ts-specific functions. For learning functional programming, I found [Functors, Applicatives And Monads In Pictures](https://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html) (written for Haskell but there's a JS port as well if you prefer) and the [Mostly Adequate Guide to Functional Programming](https://github.com/MostlyAdequate/mostly-adequate-guide) useful resources. – Lauren Yim Oct 21 '21 at 21:34