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

Parameterised resource acquisitions #1

Merged
merged 2 commits into from
Apr 22, 2022
Merged

Parameterised resource acquisitions #1

merged 2 commits into from
Apr 22, 2022

Conversation

CMCDragonkai
Copy link
Member

@CMCDragonkai CMCDragonkai commented Apr 22, 2022

Description

There's a usecase to allow one to acquire resources, where certain resources depend on other resources.

withF({
  a: async () => [async () => {}, 1],
  get b() { return async () => [async () => {}, this.a] as const; },
}, async ({ a, b }) => {
  // a is a number 1
  // b is also number 1?
});

In the above example, the b is a resource that depends on the a resource.

I'm using the new getter feature of JS objects, where this is used to refer to the original object.

However here, this.a would actually be the resource acquisition, and not the resource actually acquired.

The types of Resources would change to like this:

type ResourceAcquires = { [key: string]: ResourceAcquire<unknown> };

type Resources<T extends Readonly<ResourcesObject>> = {
  [K in keyof T]: T[K] extends ResourceAcquire<infer R> ? R : never;
};

There are a few problems though:

  1. Objects don't have a strict order. And what we are trying to do is to use a self-referential object with lambdas (using get prop) providing lazy evaluation. This means that infinite loops can happen.
  2. Use get prop is what keeps the this referencing the object. But we don't actually want the property of the ResourceAcquires. We want the Resource itself. Therefore this.a isn't exactly accurate unless we are going to change what this points to, and make it point to the actual realised resources.
  3. If we do this, then ordering becomes an issue, because this.a may not yet be acquired at that point.
  4. One way to solve this is with "memoization". So that way this.a can be referred to and you can acquire the resource by first acquiring b. This means you get a sort of a DAG of acquisitions.
  5. This would require that resource acquisition be tracked and so if it is already acquired, it will return the same result. Of course the second resource acquisition has no use for the releaser so you would want to use this.a[1] to get the Resource.

Alternatively since ResourceAcquire is already functions, you can instead pass in an object referencing the resulting resources.

withF({
  a: async () => [async () => {}, 1],
  b: async ({ a }) => [async () => {}, a],
}, async ({ a, b }) => {
  // a is a number 1
  // b is also number 1?
});

In this situation, the a is actually the Resource and not the ResourceAcquire<A>.

However we still have a problem, how do we ensure ordering here. Now b has a hard dependency of a, but calling it will pass the acquired resources object. You wouldn't know the order required.

It seems like we need to use a key-value list instead:

withF([
  ['a', async () => [async () => {}, 1]],
  ['b', async ({ a }) => [async () => {}, a]],
], async ({ a, b }) => {
  // a is a number 1
  // b is also number 1?
});

Here the array enables users to control which resources must be allocated first. The addition of the key in the tuple is label for each resource.

Then subsequent acquisitions can refer to earlier acquisitions through the label.

It's possible to do this without explicit labels with just indexes:

withF([
  async () => [async () => {}, 1],
  async ([a]) => [async () => {}, a],
  async ([,b]) => [async () => {}, b],
], async ([ a, b, c ]) => {
  // a is a number 1
  // b is also number 1?
  // c is also number 1?
});

This final variant seems the most backwards compatible with what we have already, and it should be a rather simple change. Each subsequent acquisition call is given the array of resources already acquired. Now subsequent acquisitions can be parameterised by earlier acquisitions.

Issues Fixed

Final checklist

  • Domain specific tests
  • Full tests
  • Updated inline-comment documentation
  • Lint fixed
  • Squash and rebased
  • Sanity check the final build

@CMCDragonkai
Copy link
Member Author

CMCDragonkai commented Apr 22, 2022

This PR has achieved it, but the types are not automatically propagated.

    await withF(
      [
        async () => [async () => {}, 1],
        async ([a]) => [async () => {}, a + 1],
        async ([, b]) => [async () => {}, b + 1],
      ],
      async ([a, b, c]) => {
        expect(a).toBe(1);
        expect(b).toBe(2);
        expect(c).toBe(3);
      },
    );

The a and b are inferred to be any. I'm not able to make a to be inferred to the first resource. It would probably require more type-machinery to make the types work.

So for now users just have to do their own assertions with something like:

    await withF(
      [
        async () => [async () => {}, 1],
        async ([a]) => [async () => {}, a + 1 as number],
        async ([, b]) => [async () => {}, b + 1 as number],
      ],
      async ([a, b, c]) => {
        expect(a).toBe(1);
        expect(b).toBe(2);
        expect(c).toBe(3);
      },
    );

Or whatever the return type will be.

@CMCDragonkai CMCDragonkai changed the title WIP: Parameterised resource acquisitions Parameterised resource acquisitions Apr 22, 2022
@CMCDragonkai CMCDragonkai merged commit a98f9ee into master Apr 22, 2022
@CMCDragonkai CMCDragonkai deleted the recursive branch April 22, 2022 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

1 participant