Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memoize functions for Task and TaskEither #148

Open
stevebluck opened this issue Oct 17, 2022 · 6 comments
Open

Memoize functions for Task and TaskEither #148

stevebluck opened this issue Oct 17, 2022 · 6 comments

Comments

@stevebluck
Copy link

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.

@tmueller
Copy link

tmueller commented Oct 20, 2022

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.

@mlegenhausen
Copy link
Contributor

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

@DenisFrezzato
Copy link

I did this gcanti/fp-ts-contrib#84.

@samhh
Copy link
Owner

samhh commented Oct 24, 2022

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

@stevebluck
Copy link
Author

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

@mlegenhausen
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants