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

adopt EReturn type #9011

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

adopt EReturn type #9011

wants to merge 4 commits into from

Conversation

turadg
Copy link
Member

@turadg turadg commented Feb 28, 2024

Description

While working on another PR, I had to type out ReturnType<Awaited<ReturnType< again and I wished for a utility type.

This PR first made an ExoObj type but has changed to use EReturn now available in Endo.

Security Considerations

n/a

Scaling Considerations

n/a

Documentation Considerations

Not necessary

Testing Considerations

CI

Upgrade Considerations

n/a

@turadg turadg requested review from erights and michaelfig February 28, 2024 23:23
@turadg turadg force-pushed the ta/ExoObj branch 2 times, most recently from 47867d0 to 42d2a7f Compare February 28, 2024 23:26
Copy link
Member

@erights erights left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reluctantly LGTM. Please read the comment about other steps that should also happen, if the assumption in that comment applies. If it does not apply --- if ExoObj really would only be of interest starting at agoric-sdk --- then nevermind and this PR unreservedly LGTM.

Would be nice if the doccomment also made clear which of these cases applies.

@@ -18,3 +18,5 @@ export declare class SyncCallback<

public isSync: true;
}

export type ExoObj<T> = Awaited<ReturnType<Awaited<ReturnType<T>>>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not obvious to me why this pattern repeatedly appears, or at least slight simpler ones subsumed by this one. Please add a doccomment explaining what this type means and when it should be used.

Is that reason somehow specific to agoric-sdk, or would it apply and be useful to other users of @endo/exo? If so,

  • I'm sure you already know my feelings about introducing it this way to agoric-sdk.
  • But I understand not wanting to wait for another round of endo release and agoric-sdk sync. Attn @kriskowal @ivanlei
  • Please add a TODO at least here, that we expect this type to migrate to @endo/exo
  • Please do an equivalent PR for @endo/exo, assuming that's the right place for ExoObj to eventually end up. Or at least file an issue with the need to do that and assign it to me.

What has me at a loss is that we cannot abstract away the location that ExoObj is imported from without loss of info in the IDE. Instead, each use location mentions @agoric/internal, all of which will need to change once we've completed the deprecation dance. I don't know what to suggest about that, but if you have any clever ideas, please let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should also mention that since I don't yet understand why this pattern repeats or when it should be used, I don't yet have an opinion of whether ExoObj is a good name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name ExoObj does need more thought and I hope it’s not necessary to name. This is, I believe, an auto-unwrapping problem and @michaelfig has been very clever with those kinds of problems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kriskowal when you say "not necessary to name" do you mean some other way to get VaultDirector from prepareVaultDirector? I'm having a hard time imagining how that's possible without a utility type. Codegen could, but that's unnecessarily complex.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I withdraw my hope that we can avoid the need for a utility. For ExoObj<T>, what is T generally that requires this nested Awaited and ReturnType unboxing? Is this utility recursive or limited to a depth of two? Will there be a need for three levels? My hope is was that T was always an Exo (thus the name) and that we might change something about T to obviate the need for a utility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked, and the actual case is subtler than class vs instance. If it were class vs instance, we'd only need ReturnType<ReturnsType< without the Awaited<s. Rather, the direct class creation is wrapped with a function that defines one class per call to the function and returns some function making function, like the maker function returned by the class definition. If this wrapping function needs to await anything in its logic around the class creation, then the wrapping function is made async and it returns a promise for the instance making function.

If the instance making function is the one returned by the class definition, fine. But it itself might be a wrapper around the base instance making function. It still returns one instance each time it is called. But if any of the other things it does needs to await anything, then it returns a promise for the instance instead.

That accounts for both Awaited<s. When there are no promises in the way, the extra Awaited<s are harmless.

@turadg , is that right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More than 57bce86 ?

Yes please. There's a lot going on here!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't given thought to makeExo which makes a usable obj. I could make this type conditional on whether the param type is a callable function.

Please don't fold makeExo into the same case. It isn't and should not look like it is.

Copy link
Member

@erights erights Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makeExo is at the same level as the second phase, of an instance making function that makes an instance each time it is called. For makeExo itself, we would only need ReturnType<. But the instance making function might be a wrapper around makeExo that does some other stuff around the call to makeExo. If any of that might need to await something, then the wrapping function will be async and we'd need the Awaited<ReturnType<. Does that make sense?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that make sense?

What you said in that paragraph matches my understanding, but I don't see how it supports the previous statement,

Please don't fold makeExo into the same case. It isn't and should not look like it is.

I meant the result of a makeExo call, just as the others are results of defineExo calls. E.g.,

/** @type {BuildQuestion} */
const buildQuestion = (questionSpec, counterInstance) => {
const questionHandle = makeHandle('Question');
/** @type {Question} */
return makeExo('question details', QuestionI, {
getVoteCounter() {
return counterInstance;
},
getDetails() {
return harden({
...questionSpec,
questionHandle,
counterInstance,
});
},
});
};

So supporting,

/** @typedef {ExoObj<typeof buildQuestion>`} Question

That has only one level of extracting the return type, vs two with the Exo class case.

Are you ok with the ExoObj supporting both levels? WDYT of Michael's idea to have the fully general EReturn?

Copy link
Member

@kriskowal kriskowal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming that @agoric/internal is not de facto external, putting ExoObj there means we can punt the question of a good name, but I would encourage having the naming and documentation conversation early. The name should communicate why it exists and when you would use it in opposition to the alternatives.

The name shouldn’t include the Obj abbreviation. Having to guess whether an abbreviation in play makes all names effectively unguessable and at least harder to memorize.

Can you take a pass at documenting ExoObj?

packages/inter-protocol/src/reserve/assetReserveKit.js Outdated Show resolved Hide resolved
@@ -18,3 +18,5 @@ export declare class SyncCallback<

public isSync: true;
}

export type ExoObj<T> = Awaited<ReturnType<Awaited<ReturnType<T>>>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name ExoObj does need more thought and I hope it’s not necessary to name. This is, I believe, an auto-unwrapping problem and @michaelfig has been very clever with those kinds of problems.

Comment on lines 23 to 26
/**
* Utility for the type of an Exo object once prepared and instantiated.
*/
type ExoObj<T> = Awaited<ReturnType<Awaited<ReturnType<T>>>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a little recursion, this becomes useful not just for Exos:

Suggested change
/**
* Utility for the type of an Exo object once prepared and instantiated.
*/
type ExoObj<T> = Awaited<ReturnType<Awaited<ReturnType<T>>>>;
/**
* Find the eventual return type of a specimen, recursing to function
* return values and promise fulfilments.
*/
type EReturn<T> = T extends (...args: any[]) => infer R ? EReturn<R> :
T extends PromiseLike<infer U> ? EReturn<U> :
T;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be more general, yes. I think one for Exos has some didactic value so even if we had this I'd suggest having both.

What use cases do you see for a general EReturn?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, since having EReturn would cover all the cases I don't think it's worth naming another type.

@kriskowal would you be ok with an EReturn type in Endo? You expressed some concern earlier about recursive types.

const makeHi = () => Promise.resolve('hi' as const);
type Hi = EReturn<typeof makeHi>;
const hi = await makeHi();
expectType<'hi'>(hi);
expectType<Hi>(hi);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also the case?

const lol = async () => async () => async () => 'lol';
type LoL = EReturn<typeof lol>;
expectType<'lol'>(LoL);

That would seem weird to me, recursively unwrapping both promise and return type. I would think it safer to be more specific than an arbitrary number of niladic applications.

Is it possible to have appropriate type extractors with names that resemble the corresponding constructors? I like having Return in the name. I recall @erights had a table with all the Exo constructor patterns named. Maybe that would help inform a family of return-type-of operator names.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be the case, yes.

I would think it safer to be more specific than an arbitrary number of niladic applications.

What hazard(s) are you defending against?

Is it possible to have appropriate type extractors with names that resemble the corresponding constructors?

Certainly. It's just more to write, maintain and require people to learn.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect EReturn<T> to be shallow, so it could be used to eventually capture the next function signature. It would be useful for remote functions. Can suggest reserving and pitch EUltimateReturn<T>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kriskowal kriskowal dismissed their stale review March 22, 2024 18:54

Lifting my foot.

@turadg turadg mentioned this pull request Dec 11, 2024
turadg added a commit to endojs/endo that referenced this pull request Dec 12, 2024
incidental

## Description

Migrated from Agoric/agoric-sdk#9011

Define an `EReturn` type for the `Awaited<ReturnType<` pattern.

### Security Considerations

none
### Scaling Considerations

none

### Documentation Considerations

This will appear in TSdoc

### Testing Considerations

has tests

### Compatibility Considerations

none

### Upgrade Considerations

none
@turadg turadg requested a review from a team as a code owner January 24, 2025 19:02
@turadg turadg requested a review from AgoricTriage January 24, 2025 19:02
Copy link

cloudflare-workers-and-pages bot commented Jan 24, 2025

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: f442482
Status: ✅  Deploy successful!
Preview URL: https://57b08b3a.agoric-sdk.pages.dev
Branch Preview URL: https://ta-exoobj.agoric-sdk.pages.dev

View logs

@turadg turadg changed the title feat(types): ExoObj utility adopt EReturn type Jan 24, 2025
@turadg turadg marked this pull request as draft January 24, 2025 19:06
@turadg turadg marked this pull request as ready for review January 26, 2025 18:06
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

Successfully merging this pull request may close these issues.

4 participants