typescriptfunctional-programmingfp-ts

How to use the Task monad? (fp-ts)


import * as T from 'fp-ts/lib/Task'
import { pipe, flow } from 'fp-ts/lib/function'

const getHello: T.Task<string> = () => new Promise((resolve) => {
  resolve('hello')
})

I understand the purpose of Task and why is it important. The thing is that I don't know how to use it properly or compose with it, really.

If I just call getHello(), it will give me Promise<pending>:

console.log(getHello()) // returns Promise<pending>

if I do this, however:

const run = async () => {
  const hello = await getHello()
  console.log(hello) // prints 'hello'
}

it works.

but this:

const waitAndGet = async () => {
  return await getHello()
}

console.log(waitAndGet()) // prints Promise<pending>

doesn't.

Moreover, how would I be able to compose with it? Like so:

const getHelloAndAddWorld = flow(
  getHello(),
  addAtEnd('world')
)

Solution

  • First, let’s understand what Task really is.

    export interface Task<A> {
      (): Promise<A>
    }
    
    // note that this could also be written as
    export type Task<A> = () => Promise<A>
    

    A Task is simply a function that returns a Promise, so in your example calling getHello would return a Promise<string>.

    console.log(getHello()) is the same as console.log(Promise.resolve('hello')), so this is why it would log something like Promise {<fulfilled>: "hello"}, Promise<pending>, or something else instead of hello:

    // Promise.resolve(foo) is the same as new Promise(resolve => resolve(foo))
    const getHello = () => Promise.resolve('hello')
    console.log(getHello())

    For more information on promises, I recommend reading ‘Using Promises’ on MDN.


    As for how to compose with it, since Task is a Monad you can use map, ap, chain, apSecond etc.

    For example, let’s say addAtEnd was defined like this:

    const addAtEnd = (b: string) => (a: string): string => a + b
    

    You can use this with getHello() by using Task.map:

    import * as T from 'fp-ts/Task'
    import { pipe } from 'fp-ts/function'
    
    // type of map:
    // export declare const map: <A, B>(f: (a: A) => B) => (fa: Task<A>) => Task<B>
    
    // Task<string> which, when called, would resolve to 'hello world'
    const getHelloAndAddWorld = pipe(
      getHello,
      T.map(addAtEnd(' world'))
    )
    
    // same as
    const getHelloAndAddWorld = T.map(addAtEnd(' world'))(getHello)
    

    Or if you wanted to log the value of that, you could use chainIOK and Console.log:

    import * as Console from 'fp-ts/Console'
    
    // type of T.chainIOK:
    // export declare function chainIOK<A, B>(f: (a: A) => IO<B>): (ma: Task<A>) => Task<B>
    
    // type of Console.log:
    // export declare function log(s: unknown): IO<void>
    
    // Note that IO<A> is a function that (usually) does a side-effect and returns A
    // (() => A)
    
    // Task<void>
    const logHelloAndWorld = pipe(
      getHelloAndAddWorld,
      T.chainIOK(Console.log)
    )
    
    // same as
    const logHelloAndWorld = pipe(
      getHello,
      T.map(addAtEnd(' world')),
      T.chainIOK(Console.log)
    )
    

    To execute Tasks, simply call it:

    logHelloAndWorld() // logs 'hello world'
    

    For a simple introduction to functors, applicatives, and monads, Adit’s ‘Functors, Applicatives, And Monads In Pictures’ or Tze-Hsiang Lin’s JavaScript version of that are some good starting points.