【问题标题】:fp-ts: How do I "pull up" a nested `Either`/`TaskEither` to the outer type?fp-ts:如何将嵌套的`Either`/`TaskEither`“拉起”到外部类型?
【发布时间】:2021-07-17 14:54:50
【问题描述】:

我正在学习一些fp-ts。要创建我遇到的问题的程式化版本,假设我想创建一个不存在的表,所以我必须查询数据库:一个容易出错的异步操作。如果该表不存在,我想创建它:另一个容易出错的异步操作。进一步假设错误类型都是字符串(尽管我也想知道如何在需要时创建联合错误类型),并且成功创建时返回的值是数字 ID。

简而言之,看看表是否存在,如果不存在,则创建它——在整个过程中都有可能出现错误。关键是我希望两个错误都反映在最外面的类型中:TaskEither<string, Option<number>>。问题是我不确定如何避免获得TaskEither<string, Option<TaskEither<string, number>>>。也就是说,我不知道将Option 中的错误拉起并将其合并为最外层错误的最佳方法。

(也许这涉及到序列或可遍历?我还在学习这些。)

关于一些代码:

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()))
  );
};

【问题讨论】:

    标签: typescript fp-ts


    【解决方案1】:

    我相信我想通了,当然欢迎任何更正。

    我认为我需要将Option 向下“推”到嵌套的TaskEither 中,而不是从Option 中“拉”上TaskEither,以便嵌套放置TaskEither 的层s 彼此相邻,允许通过 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)
              )
        )
      );
    

    除了TE.chainW 替换TE.chain 之外,我似乎也可以通过此代码处理有关如果错误类型不同我会做什么的附带问题。

    【讨论】:

      【解决方案2】:

      看来你自己想出来了 :) 如果有帮助,我已经将带有错误的示例实现为可区分的联合,以便您可以轻松识别调用 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)
            )
          )
        );
      };
      

      如果来自tableExistscreateTable 的错误类型不同,您正确地确定需要使用chainWWfp-ts 函数末尾的意思是“加宽”,它通常允许类型加宽为两种类型的联合。在chainW 对应TaskEither 的情况下,这意味着错误类型将成为两种TaskEither 类型的并集(一种进入chainW,另一种在其中返回)。

      了解何时使用map 以及何时使用chain 是一个重要的基本概念,理解起来很重要。 map 允许您修改结构中的值,这是 A -&gt; B 中的一个简单函数。 chain 允许您执行另一个依赖于第一个效果的效果 - 因此您必须返回一个由您正在处理的相同效果包装的值。在这种情况下,您正在使用TaskEither,因此您传递给chain 的函数也需要是A -&gt; TaskEither&lt;E, B&gt; 类型(createTable 是,但您还需要手动处理表已经存在的情况并在那里使用TE.right(O.none)TE.of(O.none) 构建TaskEither)。

      【讨论】:

      • 有道理,谢谢!我知道一点 Rust,在这里看到相同或相似的概念很有趣(map;and_then/chain;等等)。虽然我还没有完全掌握单子、函子、应用程序等的理论,但通过看到这些理论通过语言付诸实践,我正在从实践的角度理解它们的价值和用途/像这样的库结构。非常酷的东西!
      猜你喜欢
      • 2020-08-16
      • 1970-01-01
      • 1970-01-01
      • 2022-07-19
      • 2022-01-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多