【问题标题】:How to build a correct data flow using Do-notation?如何使用 Do-notation 构建正确的数据流?
【发布时间】:2022-06-13 17:23:34
【问题描述】:

我刚开始使用 fp-ts lib 学习函数式编程世界。目前我可以理解这个库提出的函数的基本概念,但我不明白如何将它们全部粘合到单个数据流中。

我想分享一个我想要实现的用户故事,并将其用作此问题的示例。听起来是这样的:

  • 用户应该可以预约选定的专家

我知道此时这对您没有意义,但让我向您展示在同一页面上的代码看起来如何。

注意:这是伪代码,使其更具可读性

const inputData = {
  userId: 1,
  specialistId: 2,
  selectedServicesIds: ['a', 'b', 'c'],
  startTime: 'today at 12:00'
}

const user = await fetchUserById(inputData.userId)

if (user === null) {
  throw 'User not found'
}

const specialist = await fetchSpecialistById(inputData.specialistId)

if (user === null) {
  throw 'Specialist not found'
}

const workingDay = await fetchWorkingDay(inputData.specialistId, inputData.startTime)

if (workingDay === null) {
  throw 'WorkingDay not found'
}

const selectedServices = await fetchSelectedServices(inputData.specialistId, inputData.selectedServicesIds)

if (selectedServices.length < inputData.selectedServices) {
  throw 'Some selected services are not belong to the selected specialist'
}

const selectedServicesDuration = calculateDuration(selectedServices)
const appointmentEndTime = addMinutes(inputData.startTime, selectedServicesDuration)

const existingAppointments = await fetchAppointmentsOfSpeciallist(inputData.specialistId)

const isAppointmentOverlapExistingAppointments = isOverlaps(existingAppointments, inputData.startTime, appointmentEndTime)

if (isAppointmentOverlapExistingAppointments) {
  throw 'Appointment overlap existing appointments'
}

return new Appointment(inputData.userId, inputData.specialistId, ...)

如您所见,这是典型的命令式代码:

  1. 获取输入数据
  2. 从数据库中获取数据
  3. 应用验证
  4. 返回结果

现在我使用 fp-ts 和 Do-notation 能够实现的目标

  pipe(
    RTE.Do,
    RTE.apS('user', fetchUserById(args.input.clientId)),
    RTE.apSW('specialist', fetchSpecialistById(args.input.specialistId)),
    RTE.apSW('workingDay', fetchWorkingDay(args.input.specialistId, args.input.startDateTime)),
    RTE.apSW('assignedServices', getAssignedServicesOfSpecialist(args.input.specialistId, args.input.servicesIds))
    RTE.map({ user, specialist, workingDay, assignedServices } => {
       // Do I need to write all logic here? 
    })

如您所见,获取相关数据的并行请求很少,但不知道下一步该做什么。如果我只是将前面示例中的命令式逻辑放在 RTE.map 函数中,它看起来就像我用一些 fp-ts 函数包装了命令式代码。

您能否给我一个建议,告诉我如何将其拆分为不同的功能以及如何将它们粘合在一起?

【问题讨论】:

    标签: fp-ts


    【解决方案1】:

    重构代码以使用fp-ts 时的关键观察是依赖EitherTaskEither 来传达有关错误的信息。然后,通过组合,您可以使用更简单的工作流程和精确的错误处理来构建更复杂的工作流程,因为有关可能错误的信息存储在 TypeScript 类型中。

    在您的情况下,fp-ts 代码的 sn-p 是编写该逻辑的一种有效方式。您在该管道中使用的每个单独的函数(fetchUserByIdfetchSpecialistByIdfetchWorkingDaygetAssignedServicesOfSpecialist)都应该返回ReaderTaskEither&lt;R, E, A&gt;(或者只是TaskEither&lt;E, A&gt;,因为我没有看到R在您的代码)。他们每个人都应该做自己的错误处理。例如,fetchUserById 可以返回类似于TaskEither&lt;{ type: 'user-not-found' }, User&gt; 的类型,其中第一个泛型参数确定此函数可能返回的错误。

    如果每个单独的函数处理它获取的数据并返回错误或该数据,那么在最终的map 中,您可以根据所有获取的信息进行最后的数据处理(此时必须有效, 因为TaskEither 负责错误传播,所以如果至少有一个错误,map 将不会被调用。

    可能的改进

    使用TaskEither 代替ReaderTaskEither

    您的fp-ts sn-p 使用RTE,我假设它是ReaderTaskEither。但是,它不使用该类型的 Reader 部分。它总是引用args,我假设它是父作用域中的一个变量。因此,您可以简化此代码并改用TaskEither 类型。

    处理最后一个map中可能出现的错误

    如果在map 中完成的最终数据组合可能会导致一些错误,您可能需要使用taskEither.chainEitherK 来返回结果:

    TE.chainEitherK(({ user, specialist, workingDay, assignedServices }) => {
      if (someCondition(user, specialist)) {
        return E.left({ type: 'some-error' });
      }
    
      // ...
    
      return E.right(/* ... */);
    });
    

    报告所有错误而不是第一个错误

    默认情况下,TaskEither 只会传播有关单个错误的信息。当您使用 apchain 时会发生这种情况 - 只有第一个错误会在最终结果中传播。

    如果您有多个可能失败的并行网络调用,您可能希望返回所有错误而不是仅返回第一个错误。

    如果您的错误具有相同的类型,您可以使用apply.sequenceTarray.sequence 将结果组合成一个Either&lt;E[], A[]&gt;。我更喜欢apply.sequenceT,因为结果的Right case 不是一个数组(长度未知),而是一个长度和值已知的元组。

    作为其中任一函数的Apply/Applicative 参数,您可以使用taskEither.getApplicativeTaskValidation,它将错误组合到一个数组中。

    getApplicativeTaskValidation 需要为数组元素提供Semigroup。最简单的情况是所有错误都具有相同的类型 - 您可以使用 array.getSemigroup&lt;MyErrorType&gt;() 获取 Semigroup&lt;MyErrorType[]&gt;

    但是,通常我的错误类型是不同的。我想这也可能是这种情况。因此,我开发了这个实用函数作为一种替代方法,它为either.Left 使用错误类型的联合:

    const tupleError = <E, A>(
      t: taskEither.TaskEither<E, A>
    ): taskEither.TaskEither<[E], A> =>
      pipe(
        t,
        taskEither.mapLeft((e) => [e])
      );
    
    const partitionErrors = <
      T extends nonEmptyArray.NonEmptyArray<either.Either<any, any>>
    >(
      results: T
    ) => {
      type ExtractLeft<T> = T extends either.Left<infer E> ? E : never;
      type WorkflowError = ExtractLeft<typeof results[number]>[number];
      const validation = either.getApplicativeValidation(
        array.getSemigroup<WorkflowError>()
      );
      return apply.sequenceT(validation)(...results);
    };
    
    pipe(
      apply.sequenceT(task.ApplyPar)(
        tupleError(fetchUserById(args.input.clientId)),
        tupleError(fetchSpecialistById(args.input.specialistId)),
        tupleError(
          fetchWorkingDay(args.input.specialistId, args.input.startDateTime)
        ),
        tupleError(
          getAssignedServicesOfSpecialist(
            args.input.specialistId,
            args.input.servicesId
          )
        )
      ),
      task.map(partitionErrors),
      taskEither.match(
        (errors) => {
          // TODO: handle errors
        },
        ([user, specialist, workingDay, assignedServices]) => {
          // TODO:
        }
      )
    );
    
    // Stub types
    interface User {}
    interface Specialist {}
    interface WorkingDay {}
    interface SelectedServices {}
    
    // Your functions should return TaskEither. The first generic argument specifies the possible error type. The second argument is for the "right" value.
    declare function fetchUserById(
      clientId: unknown
    ): taskEither.TaskEither<{ type: "user-not-found-error" }, User>;
    
    declare function fetchSpecialistById(
      specialistId: unknown
    ): taskEither.TaskEither<{ type: "specialist-not-found" }, Specialist>;
    
    declare function fetchWorkingDay(
      specialistId: unknown,
      startDateTime: unknown
    ): taskEither.TaskEither<{ type: "working-day-not-found" }, WorkingDay>;
    
    declare function getAssignedServicesOfSpecialist(
      specialistId: unknown,
      servicesIds: unknown[]
    ): taskEither.TaskEither<
      { type: "selected-services-do-not-belong-to-specialist" },
      SelectedServices
    >;
    

    【讨论】:

      猜你喜欢
      • 2021-08-14
      • 2017-11-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-10-19
      • 2021-12-27
      • 1970-01-01
      相关资源
      最近更新 更多