如何解决嵌套数据获取 fp-ts 末日金字塔
首先使用 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)
)
)
)
);
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
}
解决方法
我不知道 FP-TS,所以我只能提供一般性的答案,但基本的代数规则是相同的。
首先,monads 自然形成嵌套结构。没有办法绕过它,但您可以隐藏它,前提是您拥有正确的工具(Haskell 中的 do
符号)。不幸的是,在 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 => x === 0 ? of(x) : of(2/x)
。应用函子应该就够了:
const record = (type,{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,因为它执行效果的频率是正常情况的两倍。到目前为止,我还没有遇到问题,所以我很有信心。
,这里尝试抽象一些重复的模式:
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
)
)
)
)
)
)
)
)
);
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。