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

Add callCC to ContT #3747

Merged
merged 6 commits into from
Feb 1, 2021
Merged

Add callCC to ContT #3747

merged 6 commits into from
Feb 1, 2021

Conversation

TimWSpence
Copy link
Member

Add "call with current continuation" to ContT

@codecov-io
Copy link

codecov-io commented Jan 27, 2021

Codecov Report

Merging #3747 (f1df637) into master (88ccb7a) will increase coverage by 0.01%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##           master    #3747      +/-   ##
==========================================
+ Coverage   90.24%   90.25%   +0.01%     
==========================================
  Files         390      390              
  Lines        8865     8868       +3     
  Branches      260      267       +7     
==========================================
+ Hits         8000     8004       +4     
+ Misses        865      864       -1     

LukaJCB
LukaJCB previously approved these changes Jan 27, 2021
*/
def callCC[M[_], R, A, B](f: (A => ContT[M, R, B]) => ContT[M, R, A]): ContT[M, R, A] =
apply { cb =>
f(a => apply(_ => cb(a))).run(cb)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to see:

  1. Why we can't move cb(a) out
  2. If we should use Defer[M] anywhere to make this lazier or safer (as is done in tailRecM)

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, what did you mean by move cb(a) out?

The Defer thing is something I was worried about. I am not aware that we need it anywhere but was hoping that somebody with more experience would catch it in review if I'm wrong

Copy link
Contributor

@johnynek johnynek Jan 28, 2021

Choose a reason for hiding this comment

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

for instance:

apply { cb =>
  val cont = f { a =>
    val cba = cb(a)
    apply(_ => cba)
  }

  cont.run(cb)
}

has potentially different execution behavior, although, if we have purity, I think the same result. It isn't clear to me which is likely to be more efficient.

Secondly, if we have a Defer[M] then we could put the entire cont.run(cb) in a defer: Defer[M].defer(cont.run(cb))

Since we do that on map and flatMap, I assume there are recursive cases that would want that here as well, and if nothing else, for consistency, I guess I'd do it...

But it would be nice to find a recursive example and see if we can show we can do deep recursions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm it would appear that it is not stack-safe. I pushed a failing test for it but no amount of poking it with defer and AndThen has fixed it yet 😂

Copy link
Contributor

Choose a reason for hiding this comment

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

this seems to fix it:

oscar@Patricks-MacBook-Air cats % git diff
diff --git a/core/src/main/scala/cats/data/ContT.scala b/core/src/main/scala/cats/data/ContT.scala
index 4bef4e13c..d4864b80b 100644
--- a/core/src/main/scala/cats/data/ContT.scala
+++ b/core/src/main/scala/cats/data/ContT.scala
@@ -40,7 +40,10 @@ sealed abstract class ContT[M[_], A, +B] extends Serializable {
     // allocate/pattern match once
     val fnAndThen = AndThen(fn)
     ContT[M, A, C] { fn2 =>
-      val contRun: ContT[M, A, C] => M[A] = _.run(fn2)
+      val contRun: ContT[M, A, C] => M[A] =  { c =>
+        M.defer(c.run(fn2))
+      }
+
       val fn3: B => M[A] = fnAndThen.andThen(contRun)
       M.defer(run(fn3))
     }

in the flatMap function.

Can you try that (I pulled your PR and it worked for me).

Copy link
Member Author

Choose a reason for hiding this comment

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

😍 THANK YOU! Very good call. Just out of curiosity: what made you realize that flatMap was the issue?

I've added an explicit stack-safety test for flatMap as well (which I just assumed already existed hence not even thinking to suspect flatMap)

Copy link
Contributor

Choose a reason for hiding this comment

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

Honestly, I just looked at all the places a Defer could go and I tried it.

Afterwords, it seemed more obvious that flatMap needed one there: it was internally doing a run which starts a new stack on whatever M you have. By using a Defer, you should push that evaluation onto the same final heap used to interpret M.

I think a good rule of thumb should be: any time we do a run internally, we should wrap that inside Defer[M].defer.

Copy link
Member Author

@TimWSpence TimWSpence Feb 1, 2021

Choose a reason for hiding this comment

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

Thanks @johnynek, just trying to visualize the execution and the interaction between AndThen and Defer. Any insights much appreciated 😂

*
* {{{
* for {
* _ <- ContT.callCC( (k: Unit => ContT[IO, Unit, Unit]) =>
Copy link

Choose a reason for hiding this comment

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

Use brackets instead of parentheses to make it cleaner looking?

*/
def callCC[M[_], R, A, B](f: (A => ContT[M, R, B]) => ContT[M, R, A]): ContT[M, R, A] =
apply { cb =>
f(a => apply(_ => cb(a))).run(cb)
Copy link
Contributor

Choose a reason for hiding this comment

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

according to the principal above, I think we should put the run call in callCC inside a defer.

apply { cb =>
  val cont = f { a =>
    apply(_ => cb(a))
  }

  Defer[M].defer(cont.run(cb))
}

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 done. If you could re-trigger the build that would be great! I created #3762 to track/capture the scalacheck seed for the Order[FiniteDuration] laws failure

Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

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

request one more change and then we can merge this I think.

Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

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

This is great. Glad we have the flatMap stack safety checks too. Thanks!

@johnynek johnynek merged commit c1b179a into typelevel:master Feb 1, 2021
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.

5 participants