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

Make working with promises of promises easier #3813

Merged
merged 26 commits into from
Aug 2, 2021
Merged

Make working with promises of promises easier #3813

merged 26 commits into from
Aug 2, 2021

Conversation

SeanTAllen
Copy link
Member

Adds next_unwrap method to Promise. next_unwrap makes working
with types like Promise[Promise[Foo]] much easier to work with
compared to using next.

The idea for next_unwrap was my idea while working on a Pony client
for the GitHub REST API. The implementation itself was written by
Joe McIlvain.

Adds `next_unwrap` method to `Promise`. `next_unwrap` makes working
with types like `Promise[Promise[Foo]]` much easier to work with
compared to using `next`.

The idea for `next_unwrap` was my idea while working on a Pony client
for the GitHub REST API. The implementation itself was written by
Joe McIlvain.
@SeanTAllen SeanTAllen requested a review from a team July 31, 2021 17:03
@SeanTAllen SeanTAllen added the changelog - added Automatically add "Added" CHANGELOG entry on merge label Jul 31, 2021
@SeanTAllen
Copy link
Member Author

This needs to be squashed once approved. I'll take care of that.

I have a GitHub REST API library that an early version of will be released after this is merged and I do a ponyc release.

@SeanTAllen SeanTAllen mentioned this pull request Jul 31, 2021
@jasoncarr0
Copy link
Contributor

jasoncarr0 commented Jul 31, 2021

Are we settled on the naming here? In most contexts I see a method like this is almost always named as flat_map or rarely and_then/bind, do we have a chance to lean into one of these namings? Would flat_next or next_flatten be more suitable to reduce context switching overhead between Pony and non-Pony? I suppose this would be discussable by a tiny RFC

@SeanTAllen
Copy link
Member Author

I think Joe's next_unwrap is a good name in the context.

I think that flat_next is probably in the same ballpark as next_unwrap for "i might guess what this does" if you don't have the FP background.

Of the two that Jason mentioned, i find next_flatten to be preferable.

Of the existing and Jason's two names, I'm pretty apathetic. Any of the three is about the same to me.

@jemc you did the naming from my original "when", do you have a preference?

I have a library to release that is waiting on this, so i'd like to make any decision quickly.

Copy link
Member

@rhagenson rhagenson left a comment

Choose a reason for hiding this comment

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

No problems with this addition. Few changes suggested for formatting and wording. I also am wondering why the test does not have an assertion for both parts of the chain both "start" and "end" values?

.release-notes/next-unwrap.md Outdated Show resolved Hide resolved
.release-notes/next-unwrap.md Outdated Show resolved Hide resolved
.release-notes/next-unwrap.md Outdated Show resolved Hide resolved
.release-notes/next-unwrap.md Outdated Show resolved Hide resolved
Comment on lines 158 to 167
start("start")
inter("end")

primitive _NextUnwrapHelper
fun promise_string(s: String): Promise[String] =>
// fake do something with string, return promise of another
Promise[String]

fun complete(h: TestHelper, s: String) =>
h.assert_eq[String]("end", s)
Copy link
Member

Choose a reason for hiding this comment

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

I see an assertion for "end" here, but not one for "start" -- should a "start" assertion be added?

Copy link
Member Author

Choose a reason for hiding this comment

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

Start exists entirely so we can have something to call next_unwrap on. Think testing its value in promise_string makes the case that we are testing less clear.

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough. My rationale was from a perspective of "tests as documentation" to show where in the process "start" is accessible. For the same reason of "tests as documentation" I agree that as it is makes it clear where "end" is accessible -- which is the additional functionality here.

packages/promises/promise.pony Outdated Show resolved Hide resolved
packages/promises/promise.pony Outdated Show resolved Hide resolved
packages/promises/promise.pony Outdated Show resolved Hide resolved
@jemc
Copy link
Member

jemc commented Aug 1, 2021

I approve of using flat or flatten as a word instead of unwrap. It's definitely no worse than unwrap for describing what's going on. And since Jason sees it as better, I'm good with that. 👍

So I'd be good with next_flat, next_flatten, flat_next, or flatten_next (only the 2nd and 3rd of which were actually suggested by Jason above).

@SeanTAllen
Copy link
Member Author

@jasoncarr0 what do you think of flatten_next? I like it the most.

@SeanTAllen
Copy link
Member Author

at the moment however i have renamed to next_flatten and I'm good with that as well. I'll leave next_flatten and flatten_next choice to @jasoncarr0.

inner.next[None](
{(fulfilled: B) =>
p(fulfilled)
})
Copy link
Contributor

@jasoncarr0 jasoncarr0 Aug 1, 2021

Choose a reason for hiding this comment

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

Shouldn't we handle rejection from inner here and reject p?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what you mean. we handle in the else below.

Copy link
Contributor

@jasoncarr0 jasoncarr0 Aug 2, 2021

Choose a reason for hiding this comment

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

If we reach line 216 then there are no more sources of errors, so we'll never reach the else block.

But inner is still a future we need to respect. If inner rejects, then so should this method, but it never will, since this next call isn't passing a reject callback and so is using the default that does nothing on a reject and hence the returned lambda will never accept nor reject if inner is produced but fails.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think Jason is correct about the missing case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry @jasoncarr0 I'm still not getting it.

Do you have code that you think fixes the issue? Perhaps seeing the proposed fix will help me understand what you are talking about.

Copy link
Member Author

Choose a reason for hiding this comment

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

@jemc given that I didnt write the code, if you have time to try and figure out how to cover the two holes, I can test it. But I'm going to take a shot at the missing case I see although I'm not sure how to cover it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
})
}, {() => p.reject()})

So lesson learned make a suggestion first as that's a lot faster than trying to convey it otherwise

Copy link
Contributor

@jasoncarr0 jasoncarr0 Aug 2, 2021

Choose a reason for hiding this comment

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

Ah I see Joe made the suggestion down below and you got it working so we're good here

Copy link
Contributor

@jasoncarr0 jasoncarr0 left a comment

Choose a reason for hiding this comment

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

Looks like a case might be missing?

@SeanTAllen
Copy link
Member Author

I realized that my test case is totally bogus and I'm trying to figure out how to actually test this. Its non-trivial.

@SeanTAllen
Copy link
Member Author

ok figured it out. god that test did NOTHING. dur.

@SeanTAllen
Copy link
Member Author

correction, i figured some out.

i have other tests that are failing and... i can try out with real world stuff from my library that this started in and they work, so... arg.

@SeanTAllen
Copy link
Member Author

@jemc we definitely missed a case.

There outer promise is always using RejectAlways which doesnt send down the chain to the inner.

Comment on lines 217 to 220
{(fulfilled: B) =>
p(fulfilled)
})
else
Copy link
Member

@jemc jemc Aug 2, 2021

Choose a reason for hiding this comment

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

@SeanTAllen - I think something like this is the right fix that @jasoncarr0 was pointing out.

I didn't try compiling it so there might be something slightly wrong but it looks correct to me.

Suggested change
{(fulfilled: B) =>
p(fulfilled)
})
else
{(fulfilled: B) => p(fulfilled) },
{() : B? => p.reject(); error })
else

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 sadly doesnt compile

Copy link
Member Author

Choose a reason for hiding this comment

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

/home/sean/code/ponylang/ponyc/packages/promises/promise.pony:226:13: argument not a subtype of parameter
{() : B? => p.reject(); error})
^
Info:
/home/sean/code/ponylang/ponyc/packages/promises/promise.pony:226:13: argument type is {(): B ?}[A #share, B #share] iso^
{() : B? => p.reject(); error})
^
/home/sean/code/ponylang/ponyc/packages/promises/promise.pony:48:5: parameter type is Reject[None val] iso^
rejected: Reject[B] = RejectAlways[B])
^

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

Copy link
Member Author

Choose a reason for hiding this comment

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

This compiles:

            {(fulfilled: B) => p(fulfilled)},
            {()? => p.reject(); error})

@SeanTAllen
Copy link
Member Author

@jasoncarr0 @jemc what is the test case for the issue that Jason pointed out?

@SeanTAllen
Copy link
Member Author

@jasoncarr0 can you review the new tests and tell me if you think anything is missing?

@SeanTAllen
Copy link
Member Author

SeanTAllen commented Aug 2, 2021

This test also fails:

class iso _TestFlattenNextStartFulfillErrors is UnitTest
  fun name(): String => "promises/Promise.flatten_next/start_fulfill_errors"
  fun apply(h: TestHelper) =>
    let initial_string = "foo"
    let expected_reject_string = "rejected"

    h.long_test(2_000_000_000)
    h.expect_action(initial_string)
    h.expect_action(expected_reject_string)

    let start = Promise[String]
    let inter = start.flatten_next[String](
      _FlattenNextStartHelper~error_fulfill(h, initial_string),
      _FlattenNextStartHelper~reject_fail_if_called(h)
    )
    inter.next[None](
      _FlattenNextInterHelper~fulfill_fail_if_called(h),
      _FlattenNextInterHelper~reject_expected(h, expected_reject_string)
    )

    start(initial_string)

The issue as can be seen with https://playground.ponylang.io/?gist=4b958c4198f8986b2b95d554b8476dcc is that an error in fulfill apparently shouldnt cause that same reject to occur only the ones later down the chain. That's an existing bug that we didn't replicate.

Nope. Its a bug on our side. I fixed it.

@SeanTAllen
Copy link
Member Author

@jemc one of the bug fixes though means that we dont need rejected in the fulfill block (that makes sense) so it should be easier to set up the reject handler wrapper although i am struggling with it at the moment.

@SeanTAllen
Copy link
Member Author

Ok I have all my tests passing.

@jasoncarr0 please review my tests and see what cases you think are missing and ill add.

Copy link
Contributor

@jasoncarr0 jasoncarr0 left a comment

Choose a reason for hiding this comment

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

Test contents look good but the completes can be eager for some of the checks


let start = Promise[String]
let inter = start.flatten_next[String](
_FlattenNextStartHelper~error_fulfill(h, initial_string),
Copy link
Contributor

Choose a reason for hiding this comment

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

The error fulfill method up here seems to have a slight problem because this isn't the end of the test,
but error_fulfill marks complete(true)

fun error_fulfill(h: TestHelper, expected: String, actual: String) ? =>
h.assert_eq[String](expected, actual)
h.complete_action(expected)
h.complete(true)
Copy link
Contributor

@jasoncarr0 jasoncarr0 Aug 2, 2021

Choose a reason for hiding this comment

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

The completion down here could end the test early for line 189

Suggested change
h.complete(true)

Copy link
Member Author

Choose a reason for hiding this comment

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

No, this code is correct. This isn't called as part of the test in 189. There are two different primitives, one for Start, one for Inter. If this completion was on Start and not Inter that would be true.


fun reject_expected(h: TestHelper, action: String) =>
h.complete_action(action)
h.complete(true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly this can complete too early e.g.at line 238

Suggested change
h.complete(true)

Copy link
Member Author

Choose a reason for hiding this comment

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

same as above, there are two reject_expected. One for Start and one for Inter. This is the one for Inter and the test is complete when reject_expected is called. There is also a reject_expected on the Start primitive that doesn't complete.

Note there are no complete calls on _FlattenNextStartHelper

@SeanTAllen
Copy link
Member Author

@jasoncarr0 I believe this is ready to be squashed and merged. See my replies re: tests, those complete calls are correct as they are on _FlattenNextInterHelper not _FlattenNextStartHelper. If they were on _FlattenNextStartHelper, there would be a problem with complete being called early, but they aren't.

@SeanTAllen
Copy link
Member Author

I did some renaming of things in the test and because this is fairly complicated, added english lanuage descriptions of what is and is not expected for each test.

@SeanTAllen
Copy link
Member Author

I think this is ready for a squash and merge. Once approved, I'll do the squash so I can update the commit comment to match the changes this underwent since the PR was opened.

.release-notes/next-unwrap.md Outdated Show resolved Hide resolved
@SeanTAllen SeanTAllen merged commit 88782c5 into main Aug 2, 2021
@SeanTAllen SeanTAllen deleted the next-unwrap branch August 2, 2021 17:11
github-actions bot pushed a commit that referenced this pull request Aug 2, 2021
github-actions bot pushed a commit that referenced this pull request Aug 2, 2021
.release-notes/next-unwrap.md Show resolved Hide resolved
packages/promises/promise.pony Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog - added Automatically add "Added" CHANGELOG entry on merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants