简体   繁体   中英

fp-ts: How do I "pull up" a nested `Either`/`TaskEither` to the outer type?

I am learning some fp-ts . To create a stylized version of an issue I'm having, assume I want to create a table if it doesn't exist, so I must query the database: a fallible, async operation. If that table doesn't exist, I want to create it: another fallible, async operation. Assume further that the error types are both strings (though I'd also like to know how to create a union error type if needed), and that the value returned on successful creation is a numerical ID.

In short, see if the table is there, if it isn't, create it—with errors a possibility all along the way. The key is that I want both errors reflected in the outermost type: a TaskEither<string, Option<number>> . The problem is that I'm not sure how to avoid getting a TaskEither<string, Option<TaskEither<string, number>>> . That is, I don't know the best way to pull the error inside the Option up and coalesce it into the outermost error.

(Perhaps this involves sequences or traversables? I'm still learning about those.)

On to some code:

import { taskEither as TE, option as O } from "fp-ts";
import { pipe } from "fp-ts/lib/function";

// tableExists: () => TE.TaskEither<string, boolean>
// createTable: () => TE.TaskEither<string, number>

// I want this to represent both possible errors. Currently a type error.
// -------------------------------vvvvvv
const example = (): TE.TaskEither<string, O.Option<number>> => {
  return pipe(
    tableExists(),
    // How to pull the possible `left` up to the outermost type?
    // ------------------------------------------vvvvvvvvvvvvv
    TE.map((exists) => (exists ? O.none : O.some(createTable()))
  );
};

It seems like you figured it out on your own:) In case it's helpful, I've implemented the example with the error as a discriminated union so that you can easily identify which error occurred when you call example .

import * as TE from 'fp-ts/lib/TaskEither'
import * as O from 'fp-ts/lib/Option'
import { pipe } from "fp-ts/lib/function";

declare const tableExists: () => TE.TaskEither<string, boolean>
declare const createTable: () => TE.TaskEither<string, number>

// Discriminated union so you can easily identify which error it is
type ExampleErr = { tag: "TableExistsError", error: unknown } | { tag: "CreateTableError", error: unknown }

const example = (): TE.TaskEither<ExampleErr, O.Option<number>> => {
  return pipe(
    tableExists(),
    TE.mapLeft(error => ({ tag: "TableExistsError" as const, error })),
    TE.chainW(exists => exists ?
      TE.right(O.none) :
      pipe(
        createTable(),
        TE.mapLeft(error => ({ tag: "CreateTableError" as const, error })),
        TE.map(O.some)
      )
    )
  );
};

You correctly identified that you need to use chainW if the error types from tableExists and createTable are different. The W at the end of a function in fp-ts means "widen" and it generally allows the type to widen to the union of two types. In the case of chainW for TaskEither , that means that the error type will become the union of the two TaskEither types (the one going into chainW and the one being returned inside it).

Understanding when to use map and when to use chain is a fundamental concept that is important to understand well. map allows you to modify a value inside your structure, it's a simple function from A -> B . chain allows you to perform another effect which relies on the result of the first one - so you must return a value which is wrapped by the same effect you are dealing with. In this case you are working with TaskEither , so the function you pass to chain also needs to be of type A -> TaskEither<E, B> (which createTable is, but you also need to manually handle the case where the table already exists and construct the TaskEither there using TE.right(O.none) or TE.of(O.none) ).

I believe I figured this out, though of course welcome any corrections.

Rather than "pulling" the TaskEither up from within the Option , I think I needed to "push" the Option down into the nested TaskEither so that the nesting put the layers of TaskEither s next to each other, allowing flattening them via chain .

const example = (): TE.TaskEither<string, O.Option<number>> =>
  pipe(
    tableExists(),
    TE.chain((exists) =>
      exists
        ? TE.of(O.none)
        : pipe(
            createTable(),
            TE.map(O.of)
          )
    )
  );

My side question about what I'd do if the error types were different also appears to be handled by this code, except TE.chainW replaces TE.chain .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM