Memoize functions for Task and TaskEither
I have found the need to cache some data that is produced asynchronously. It would be great to have the memoize function available in an async context.
With a TaskEither I wouldn't want to cache the Left though. Maybe something similar can be done with what's in Function/memoize with the eq or a predicate aka E.isRight?
I have tried implementing this myself but tripped up around race conditions.
Something along the lines of:
pipe(
TE.right(1),
memoize(E.isRight)
)
Not sure if it's possible without a concrete implementation around TaskEither's.
Memoization of Task could be done this way:
import { memoize as memoizeF } from "fp-ts-std/Function";
import { memoize as memoizeIO } from "fp-ts-std/IO";
import type { Eq } from "fp-ts/Eq";
import { flow } from "fp-ts/function";
import type { Task } from "fp-ts/Task";
export const memoizeTask = memoizeIO as <A>(f: Task<A>) => Task<A>;
export const memoizeTaskK =
<A>(eq: Eq<A>) =>
<B>(f: (x: A) => Task<B>) =>
memoizeF(eq)(flow(f, memoizeTask));
Not sure about memoizing TE though.
@stevebluck what problems do you encounter?
We would currently use something like this:
import { Eq } from 'fp-ts/Eq';
import * as M from 'fp-ts/Map';
import * as O from 'fp-ts/Option';
import type { Predicate } from 'fp-ts/Predicate';
import { Task } from 'fp-ts/Task';
import { pipe } from 'fp-ts/function';
export const memoizeK =
<A>(eq: Eq<A>) =>
<B>(f: (a: A) => Task<B>, shouldCache: Predicate<B>): ((a: A) => Task<B>) => {
const cache = new Map<A, Promise<B>>();
return (a) =>
pipe(
cache,
M.lookup(eq)(a),
O.fold(
() => () => {
const p = f(a)();
cache.set(
a,
p.then((b) => {
if (shouldCache(b) === false) {
cache.delete(a);
}
return b;
}),
);
return p;
},
(p) => () => p,
),
);
};
Then you can use the function like this
const memoized = memoizeK(Str.Eq)((a: string) => TE.right(a), E.isLeft);
I did this https://github.com/gcanti/fp-ts-contrib/pull/84.
@DenisFrezzato Wouldn't that be susceptible to a race condition for async types? i.e. I'd anticipate task memoisation reusing an in-flight promise.
@mlegenhausen this is perfect and is what I was looking for.
In my case I have a TaskEither called getShippingPass: TE.TaskEither<Error, ShippingPass>. It doesn't accept any arguments, it's just a TE. I'd like to cache the response when the task returns an E.right.
I have adapted your function to work with my use case but I wonder if there is a way to do it without passing in any arguments? I guess we would just hardcode the cache key.
You can infer a memoize function from memoizeK like
export const memoize = <B>(f: Task<B>, shouldCache: Predicate<B>): Task<B> => memoizeK({ equals: constTrue })(() => f, shouldCache)(undefined);