简体   繁体   English

fp-ts:如何将嵌套的`Either`/`TaskEither`“拉起”到外部类型?

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

I am learning some fp-ts .我正在学习一些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.进一步假设错误类型都是字符串(尽管我也想知道如何在需要时创建联合错误类型),并且成功创建时返回的值是数字 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>> .关键是我希望这两个错误都反映在最外面的类型中:a TaskEither<string, Option<number>> The problem is that I'm not sure how to avoid getting a TaskEither<string, Option<TaskEither<string, number>>> .问题是我不确定如何避免获得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.也就是说,我不知道将Option中的错误向上拉并将其合并为最外层错误的最佳方法。

(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 .看起来你自己想出来了:) 如果它有帮助,我已经将带有错误的示例实现为可区分的联合,以便您可以轻松识别调用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.如果tableExistscreateTable的错误类型不同,您正确地确定了需要使用chainW 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. fp-ts中 function 末尾的W表示“加宽”,它通常允许类型加宽为两种类型的并集。 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).对于chainWTaskEither ,这意味着错误类型将成为两种TaskEither类型的并集(一种进入chainW ,另一种在其中返回)。

Understanding when to use map and when to use chain is a fundamental concept that is important to understand well.了解何时使用map以及何时使用chain是一个重要的基本概念,需要很好地理解。 map allows you to modify a value inside your structure, it's a simple function from A -> B . map允许您修改结构内的值,它是来自A -> B的简单 function 。 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. chain允许您执行另一个依赖于第一个结果的效果 - 因此您必须返回一个由您正在处理的相同效果包装的值。 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) ).在这种情况下,您正在使用TaskEither ,因此您传递给chain的 function 也需要是A -> TaskEither<E, B>类型( createTable是,但您还需要手动处理表已经存在的情况并使用TE.right(O.none)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 .我认为我需要将Option向下“推”到嵌套的TaskEither中,以便嵌套将TaskEither的层彼此相邻,从而允许通过chain将它们展平,而不是从Option中“拉”出TaskEither

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 .除了TE.chainW替换TE.chain之外,我关于如果错误类型不同我会做什么的问题似乎也由这段代码处理。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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