-
Notifications
You must be signed in to change notification settings - Fork 220
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
base: master
Are you sure you want to change the base?
adopt EReturn type #9011
Conversation
47867d0
to
42d2a7f
Compare
There was a problem hiding this 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.
packages/internal/src/types.d.ts
Outdated
@@ -18,3 +18,5 @@ export declare class SyncCallback< | |||
|
|||
public isSync: true; | |||
} | |||
|
|||
export type ExoObj<T> = Awaited<ReturnType<Awaited<ReturnType<T>>>>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.,
agoric-sdk/packages/governance/src/question.js
Lines 82 to 100 in fd864a4
/** @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
?
There was a problem hiding this 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/internal/src/types.d.ts
Outdated
@@ -18,3 +18,5 @@ export declare class SyncCallback< | |||
|
|||
public isSync: true; | |||
} | |||
|
|||
export type ExoObj<T> = Awaited<ReturnType<Awaited<ReturnType<T>>>>; |
There was a problem hiding this comment.
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.
packages/internal/src/types.d.ts
Outdated
/** | ||
* Utility for the type of an Exo object once prepared and instantiated. | ||
*/ | ||
type ExoObj<T> = Awaited<ReturnType<Awaited<ReturnType<T>>>>; |
There was a problem hiding this comment.
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:
/** | |
* 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; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Deploying agoric-sdk with Cloudflare Pages
|
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 useEReturn
now available in Endo.Security Considerations
n/a
Scaling Considerations
n/a
Documentation Considerations
Not necessary
Testing Considerations
CI
Upgrade Considerations
n/a