简体   繁体   English

嵌套数据获取 fp-ts 末日金字塔

[英]Nested Data Fetching fp-ts pyramid of doom

First jump into the world of functional programming in typescript with the fp-ts library.首先使用 fp-ts 库进入 typescript 中的函数式编程世界。

I have here a "pyramid of doom" the Egyptians would be proud of, I'm clearly doing something wrong in my approach.我在这里有一个埃及人会引以为豪的“末日金字塔”,我的方法显然做错了。 What approach should I be taking to solve my nested data fetching problem in a less imperative style?我应该采取什么方法以一种不太命令的方式解决我的嵌套数据获取问题?

The Pyramid金字塔

export const getProgramWithAllElements = (programId: FirestoreDocumentId): TE.TaskEither<FirestoreError, Program> =>
  pipe(
    getProgram(programId),
    TE.chain((program) =>
      pipe(
        getCollCollectionsFromPath(program.id),
        TE.chain((collections) =>
          pipe(
            collections,
            A.map((collection) =>
              pipe(
                getWeekCollectionsFromPath(program.id)(collection.id),
                TE.chain((weeks) =>
                  pipe(
                    weeks,
                    A.map((week) =>
                      pipe(
                        getDayCollectionsFromPath(program.id)(collection.id)(week.id),
                        TE.chain((days) =>
                          pipe(
                            days,
                            A.map((day) =>
                              pipe(
                                getExerciseCollectionsFromPath(program.id)(collection.id)(week.id)(day.id),
                                TE.map(
                                  (exercises) =>
                                    ({
                                      ...day,
                                      exercises: exercises,
                                    } as Day)
                                )
                              )
                            ),
                            A.sequence(TE.taskEither)
                          )
                        ),
                        TE.map(
                          (days) =>
                            ({
                              ...week,
                              days: days,
                            } as Week)
                        )
                      )
                    ),
                    A.sequence(TE.taskEither)
                  )
                ),
                TE.map(
                  (weeks) =>
                    ({
                      ...collection,
                      weeks: weeks,
                    } as Collection)
                )
              )
            ),
            A.sequence(TE.taskEither)
          )
        ),
        TE.map(
          (collections) =>
            ({
              ...program,
              collections: collections,
            } as Program)
        )
      )
    )
  );

Functions used in snippet片段中使用的函数

declare const getProgram: (programId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Program>;

declare const getCollCollectionsFromPath: (
  programId: FirestoreDocumentId
) => TE.TaskEither<FirestoreError, Collection[]>;

declare const getWeekCollectionsFromPath: (
  programId: FirestoreDocumentId
) => (collectionId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Week[]>;

declare const getDayCollectionsFromPath: (programId: FirestoreDocumentId) => (collectionId: FirestoreDocumentId) => (
  weekId: FirestoreDocumentId
) => TE.TaskEither<FirestoreError, Day[]>;

declare const getExerciseCollectionsFromPath: (programId: FirestoreDocumentId) => (collectionId: FirestoreDocumentId) => (
  weekId: FirestoreDocumentId
) => (dayId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Exercise[]>;

Simplified Data Model简化数据 Model

export interface Program {
    id: string;
    // Other Fields
    collections?: Collection[];
}

export interface Collection {
    id: string;
    // Other Fields
    weeks?: Week[];
}

export interface Week {
    id: string;
    // Other Fields
    days?: Day[];
}

export interface Day {
    id: string;
    // Other Fields
    exercises: ProgramExercise[];
}

export interface ProgramExercise {
    id: string;
    // Other Fields
}

I don't know FP-TS so I can only provide a general answer, but the underlying algebraic rules are the same.我不知道 FP-TS,所以我只能提供一个一般性的答案,但基本的代数规则是相同的。

First of all, monads naturally form nested structures.首先,单子自然形成嵌套结构。 There is no way around it, but you can hide it provided you have the right tool ( do notation in Haskell).没有办法绕过它,但如果你有正确的工具(Haskell 中的do符号),你可以隐藏它。 Unfortunately, in Javascript there is no way to abstract from nesting in general, but you can use generators to maintain a flat structure for deterministic computations.不幸的是,在 Javascript 中,通常无法从嵌套中抽象出来,但您可以使用生成器来维护确定性计算的平面结构。 The following code depends on the scriptum lib I maintain:以下代码取决于我维护的脚本库

 const record = (type, o) => ( o[Symbol.toStringTag] = type.name || type, o); const Cont = k => record(Cont, {cont: k}); const contOf = x => Cont(k => k(x)); const contChain = mx => fm => Cont(k => mx.cont(x => fm(x).cont(k))); const _do = ({chain, of}) => init => gtor => { const go = ({done, value: x}) => done? of(x): chain(x) (y => go(it.next(y))); const it = gtor(init); return go(it.next()); }; const log = (...ss) => (console.log(...ss), ss[ss.length - 1]); const inck = x => Cont(k => k(x + 1)); const sqrk = x => Cont(k => Promise.resolve(null).then(k(x * x))); const mainM = _do({chain: contChain, of: contOf}) (5) (function* (init) { const x = yield inck(init), y = yield sqrk(init); return [x, y]; }); mainM.cont(log)

However, as far as I can tell from your code you don't actually need monad, because your next computations don't depend on previous values, like in chain(tx) (x => x === 0? of(x): of(2/x) . Applicative functor should be just enough:但是,据我所知,您实际上并不需要 monad,因为您的下一个计算不依赖于先前的值,例如 in chain(tx) (x => x === 0? of(x): of(2/x) . 应用函子应该足够了:

 const record = (type, o) => ( o[Symbol.toStringTag] = type.name || type, o); const Cont = k => record(Cont, {cont: k}); const contMap = f => tx => Cont(k => tx.cont(x => k(f(x)))); const contAp = tf => tx => Cont(k => tf.cont(f => tx.cont(x => k(f(x))))); const contOf = x => Cont(k => k(x)); const liftA2 = ({map, ap}) => f => tx => ty => ap(map(f) (tx)) (ty); const contLiftA2 = liftA2({map: contMap, ap: contAp}); const log = (...ss) => (console.log(...ss), ss[ss.length - 1]); const inck = x => Cont(k => k(x + 1)); const sqrk = x => Cont(k => Promise.resolve(null).then(k(x * x))); const mainA = contLiftA2(x => y => [x, y]) (inck(5)) (sqrk(5)) mainA.cont(log);

As I've already mentioned you cannot use generators together with non-deterministic computations like linked lists or arrays.正如我已经提到的,您不能将生成器与链表或 arrays 等非确定性计算一起使用。 However, you can resort to applying monads in an applicative manner to ease the pain:但是,您可以通过应用方式来应用 monad 来减轻痛苦:

 const arrChain = mx => fm => mx.flatMap(fm); const chain2 = chain => mx => my => fm => chain(chain(mx) (x => fm(x))) (gm => chain(my) (y => gm(y))); const log = (...ss) => (console.log(...ss), ss[ss.length - 1]); const xs = [1,2], ys = [3,4]; main3 = chain2(arrChain) (xs) (ys) (x => [y => [x, y]]); log(main3);

As you can see there is still nested structure but it still looks tidier.如您所见,仍然存在嵌套结构,但看起来仍然更整洁。

I am not entirely sure if this technique works for all monads, because it performs effects twice as often as normal.我不完全确定这种技术是否适用于所有单子,因为它执行效果的频率是正常情况的两倍。 So far I haven't encountered an issue yet, so I'm pretty confident.到目前为止,我还没有遇到任何问题,所以我很有信心。

Here's an attempt to abstract some of the repeating patterns:下面是抽象一些重复模式的尝试:

import * as A from "fp-ts/Array";
import { flow, pipe } from "fp-ts/lib/function";

type FirestoreDocumentId = number;
interface Collection {
  id: number;
}
interface FirestoreError {
  id: number;
}
interface Day {
  id: number;
}
interface Week {
  id: number;
}
interface Program {
  id: number;
}
interface Exercise {
  id: number;
}

declare const getProgram: (
  programId: FirestoreDocumentId
) => TE.TaskEither<FirestoreError, Program>;

declare const getCollCollectionsFromPath: (
  programId: FirestoreDocumentId
) => TE.TaskEither<FirestoreError, Collection[]>;

declare const getWeekCollectionsFromPath: (
  programId: FirestoreDocumentId
) => (
  collectionId: FirestoreDocumentId
) => TE.TaskEither<FirestoreError, Week[]>;

declare const getDayCollectionsFromPath: (
  programId: FirestoreDocumentId
) => (
  collectionId: FirestoreDocumentId
) => (weekId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Day[]>;

declare const getExerciseCollectionsFromPath: (
  programId: FirestoreDocumentId
) => (
  collectionId: FirestoreDocumentId
) => (
  weekId: FirestoreDocumentId
) => (dayId: FirestoreDocumentId) => TE.TaskEither<FirestoreError, Exercise[]>;

const mapTo = <K extends string>(prop: K) => <T>(obj: T) => <E, A>(
  task: TE.TaskEither<E, A>
) =>
  pipe(
    task,
    TE.map(
      (data): T & { [P in K]: A } =>
        Object.assign({}, obj, { [prop]: data }) as any
    )
  );

const chainSequence = <E, A, B>(f: (t: A) => TE.TaskEither<E, B>) =>
  TE.chain(flow(A.map(f), A.sequence(TE.taskEither)));

const chainSequenceAndMapTo = <K extends string>(prop: K) => <E, B>(
  f: (id: number) => TE.TaskEither<E, B[]>
) => <A extends { id: number }>(task: TE.TaskEither<E, A[]>) =>
  pipe(
    task,
    chainSequence((a) => pipe(f(a.id), mapTo(prop)(a)))
  );

export const getProgramWithAllElements = (programId: FirestoreDocumentId) =>
  pipe(
    getProgram(programId),
    TE.chain((program) =>
      pipe(
        getCollCollectionsFromPath(program.id),
        chainSequenceAndMapTo("collections")((collectionId) =>
          pipe( 
            getWeekCollectionsFromPath(program.id)(collectionId),
            chainSequenceAndMapTo("weeks")((weekId) =>
              pipe(
                getDayCollectionsFromPath(program.id)(collectionId)(weekId),
                chainSequenceAndMapTo("exercises")(
                  getExerciseCollectionsFromPath(program.id)(collectionId)(
                    weekId
                  )
                )
              )
            )
          )
        )
      )
    )
  );

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

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