fp-ts-std icon indicating copy to clipboard operation
fp-ts-std copied to clipboard

Memoize functions for Task and TaskEither

Open stevebluck opened this issue 3 years ago • 6 comments

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.

stevebluck avatar Oct 17 '22 15:10 stevebluck

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.

tmueller avatar Oct 20 '22 13:10 tmueller

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

mlegenhausen avatar Oct 24 '22 08:10 mlegenhausen

I did this https://github.com/gcanti/fp-ts-contrib/pull/84.

DenisFrezzato avatar Oct 24 '22 10:10 DenisFrezzato

@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.

samhh avatar Oct 24 '22 10:10 samhh

@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.

stevebluck avatar Oct 25 '22 13:10 stevebluck

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

mlegenhausen avatar Oct 26 '22 18:10 mlegenhausen