anotherhale
anotherhale

Reputation: 175

How to chain dependent TaskEither operations in FP-TS

I am new to FP-TS and still don't quite understand how to work with TaskEither. I am attempting to asynchronously read a file and then parse the resulting string with yaml-parse-promise.

==EDIT==

I updated the code with the full contents of the file to give more context and applied some of the suggestions provided by MnZrK. Sorry I am still new to FP-TS and I am still struggling with getting the types to match up.

Now my error is with the the map(printConfig) line:

Argument of type '<E>(fa: TaskEither<E, AppConfig>) => TaskEither<E, AppConfig>' is not assignable to parameter of type '(a: TaskEither<unknown, AppConfig>) => Either<unknown, Task<any>>'.
  Type 'TaskEither<unknown, AppConfig>' is not assignable to type 'Either<unknown, Task<any>>'.
    Type 'TaskEither<unknown, AppConfig>' is missing the following properties from type 'Right<Task<any>>': _tag, rightts(2345)

[ I resolved this by using the getOrElse from TaskEither rather than from Either library]

==END EDIT==

I have successfully performed this with IOEither as a synchronous operation with this project: https://github.com/anotherhale/fp-ts_sync-example.

I have also looked at the example code here: https://gcanti.github.io/fp-ts/recipes/async.html

Full code is here: https://github.com/anotherhale/fp-ts_async-example

import { pipe } from 'fp-ts/lib/pipeable'
import { TaskEither, tryCatch, chain, map, getOrElse } from "fp-ts/lib/TaskEither";
import * as T from 'fp-ts/lib/Task';
import { promises as fsPromises } from 'fs';
const yamlPromise = require('js-yaml-promise');

// const path = require('path');
export interface AppConfig {
  service: {
    interface: string
    port: number
  };
}

function readFileAsyncAsTaskEither(path: string): TaskEither<unknown, string> {
  return tryCatch(() => fsPromises.readFile(path, 'utf8'), e => e)
}

function readYamlAsTaskEither(content: string): TaskEither<unknown, AppConfig> {
  return tryCatch(() => yamlPromise.safeLoad(content), e => e)
}

// function getConf(filePath:string){
//   return pipe(
//       readFileAsyncAsTaskEither(filePath)()).then(
//           file=>pipe(file,foldE(
//               e=>left(e),
//               r=>right(readYamlAsTaskEither(r)().then(yaml=>
//                   pipe(yaml,foldE(
//                       e=>left(e),
//                       c=>right(c)
//                   ))
//               ).catch(e=>left(e)))
//           ))
//       ).catch(e=>left(e))
// }

function getConf(filePath: string): TaskEither<unknown, AppConfig> {
  return pipe(
    readFileAsyncAsTaskEither(filePath),
    chain(readYamlAsTaskEither)
  )
}

function printConfig(config: AppConfig): AppConfig {
  console.log("AppConfig is: ", config);
  return config;
}

async function main(filePath: string): Promise<void> {
  const program: T.Task<void> = pipe(
    getConf(filePath),
    map(printConfig),
    getOrElse(e => {
      return T.of(undefined);
    })
  );

  await program();
}

main('./app-config.yaml')

The resulting output is: { _tag: 'Right', right: Promise { <pending> } }

But I want the resulting AppConfig: { service: { interface: '127.0.0.1', port: 9090 } }

Upvotes: 5

Views: 6790

Answers (1)

MnZrK
MnZrK

Reputation: 1380

All these e=>left(e) and .catch(e=>left(e)) are unnecessary. Your second approach is more idiomatic.

// convert nodejs-callback-style function to function returning TaskEither
const readFile = taskify(fs.readFile);
// I don't think there is `taskify` alternative for Promise-returning functions but you can write it yourself quite easily
const readYamlAsTaskEither = r => tryCatch(() => readYaml(r), e => e);

function getConf(filePath: string): TaskEither<unknown, AppConfig> {
  return pipe(
    readFile(path.resolve(filePath)),
    chain(readYamlAsTaskEither)
  );
}

Now your getConf returns TaskEither<unknown, AppConfig> which is actually a () => Promise<Either<unknown, AppConfig>>. If you have more specific error type than unknown, then use that instead.

In order to "unpack" the actual value, you need to have some main entry point function where you compose other stuff you need to do with your config using map or chain (ie printing it to console), then apply some error handling to get rid of Either part and finally get just Task (which is actually simply lazy () => Promise):

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

function printConfig(config: AppConfig): AppConfig {
  console.log("AppConfig is", config);
  return config;
}

function doSomethingElseWithYourConfig(config: AppConfig): TaskEither<unknown, void> {
  // ...
}

async function main(filePath: string): Promise<void> {
  const program: T.Task<void> = pipe(
    getConf(filePath),
    map(printConfig),
    chain(doSomethingElseWithYourConfig),
    // getting rid of `Either` by using `getOrElse` or `fold`
    getOrElse(e => {
      // error handling (putting it to the console, sending to sentry.io, whatever is needed for you app)
      // ...
      return T.of(undefined);
    })
  );

  await program();
}

Upvotes: 5

Related Questions