【问题标题】:Nested Data Fetching fp-ts pyramid of doom嵌套数据获取 fp-ts 末日金字塔
【发布时间】:2020-12-21 20:59:14
【问题描述】:

第一次使用 fp-ts 库进入 typescript 函数式编程的世界。

我在这里有一个埃及人会引以为豪的“末日金字塔”,我的方法显然做错了什么。我应该采取什么方法来以一种不那么强制性的方式解决我的嵌套数据获取问题?

金字塔

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

sn-p中使用的函数

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[]>;

简化数据模型

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
}

【问题讨论】:

  • 如果你想要更函数化的编程风格,不要这样使用pipe
  • getExerciseCollectionsFromPath(program.id)(collection.id)(week.id)(day.id) - 该 API 有问题。为什么不day.getExercises()(或getExercises(day))?
  • 感谢@Bergi 的回复我了解管道的使用是错误的,并且必须基于对实用程序的一些错误理解或有限的知识,在我的情况下有什么替代方案? getExerciseCollectionsFromPath(program.id)(collection.id)(week.id)(day.id) 是一个调用 Firebase Firestore NoSQL 数据库的函数。我不知道您使用 Firestore 的经验,但存储练习的子集合不知道它的父日集合。因此,您需要指定访问它的整个路径,例如(program.id)(collection.id)(week.id)(day.id)
  • 关于pipe,而不是pipe(val, g, f),只写f(g(val))。或者代替val =&gt; pipe(val, g, f),只写compose(f, g)。似乎这已经可以节省金字塔的一些缩进级别。
  • 关于集合的访问,如果你在day接口中包含了星期,星期中的集合等,你只需要传递包含所有必要标识符的day对象。

标签: typescript functional-programming fp-ts


【解决方案1】:

我不了解 FP-TS,所以只能提供一个笼统的答案,但底层的代数规则是一样的。

首先,单子自然形成嵌套结构。没有办法绕过它,但如果你有正确的工具,你可以隐藏它(do Haskell 中的符号)。不幸的是,在 Javascript 中,通常无法从嵌套中抽象出来,但您可以使用生成器来维护确定性计算的平面结构。以下代码依赖于我维护的scriptum lib

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)

但是,据我所知,您的代码实际上并不需要 monad,因为您的下一个计算不依赖于以前的值,例如 chain(tx) (x =&gt; 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);

正如我已经提到的,您不能将生成器与链表或数组等非确定性计算一起使用。但是,您可以通过应用方式来应用 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);

如您所见,仍然存在嵌套结构,但看起来仍然更整洁。

我不完全确定这种技术是否适用于所有 monad,因为它执行效果的频率是正常情况的两倍。到目前为止我还没有遇到问题,所以我很有信心。

【讨论】:

  • 这里有漂亮的代码演示。我爱record。我正在使用const struct = (T, t) =&gt; Object.assign(t, { [Symbol.toStringTag]: T.name ?? T })
  • @Thankyou 嗯,也谢谢你。我目前正在研究具有安全就地更新的仿射数组/地图。似乎突变毕竟没有那么有害。你只需要包含共享,所以我被 Rust 语言告诉了。
【解决方案2】:

这里尝试抽象一些重复模式:

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

【讨论】:

    猜你喜欢
    • 2017-06-22
    • 1970-01-01
    • 2021-10-03
    • 1970-01-01
    • 1970-01-01
    • 2021-05-10
    • 2012-06-09
    • 2021-12-09
    • 2015-12-14
    相关资源
    最近更新 更多