-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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 ContT monad #2506
Add ContT monad #2506
Conversation
Not to be Debbie Downer, but this (like all Cont implementations) still doesn’t have a stack safe tailRecM given a truly arbitrary value of Cont. Specifically, arbitrary left-associated flattens sequenced through continue should blow the stack. This is encodable via tailRecM just as it is via flatMap. There are older examples of this in the cats-effect history (IO is basically Defer + Cont). To be clear, I don’t think that’s a problem. We softened the laws on tailRecM specifically to allow this situation. Just pointing it out. I should be able to contrive a full example in terms of this when I’m not on my phone. |
It’s an interesting question how many current Monads may fail this test with better generators. It may be that this isn’t stack safe with more aggressive generators but the use of AndThen doesn’t make it totally obvious to me that is the case. The WithCont/MapCont isn’t safe I guess in recursions. In any case, without Defer this implementation fails, which I think shows a nice example of using Defer. |
to save you some time digging travis log, scalastyle was complaining
|
okay, interestingly, using Defer on each call in map and flatMap makes this a StackSafeMonad, but I went ahead and kept the custom implementation which I think should be more efficient. What do you think now @djspiewak ? I think this may actually be fully stack safe (at the expense of requiring to defer inside each map/flatMap). |
Reading and pondering… :-D |
ContT[M, A, C] { fn2 => | ||
val contRun: ContT[M, A, C] => M[A] = (_.run(fn2)) | ||
val fn3: B => M[A] = fnAndThen.andThen(contRun) | ||
M.defer(run(fn3)) |
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 stand very much corrected here. I'm convinced that this is stack-safe. I owe you an apology, @johnynek. Dear internets, please link here if you're looking for a handy example of me being wrong.
When I first reviewed this PR on my phone, it looked like a slightly different version of the trick you used in #1400, but this is considerably better. You're basically inheriting the stack-safety of the underlying monad by explicitly hitting a stack-safe join at this point and returning to its trampoline (which it must have due to Defer
). I'm not sure you even need the AndThen
, since the stack is cut by the defer
.
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.
We all make mistakes here and there. I very much admire folks who feel no shame about it. Kudos to you.
def later[M[_], A, B](fn: => (B => M[A]) => M[A]): ContT[M, A, B] = | ||
DeferCont(() => FromFn(AndThen(fn))) | ||
|
||
def tailRecM[M[_], A, B, C](a: A)(fn: A => ContT[M, C, Either[A, B]])(implicit M: Defer[M]): ContT[M, C, B] = |
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.
Can just delegate to flatMap
now. Probably slightly slower than this though.
DeferCont(() => c) | ||
} | ||
|
||
implicit def catsDataContTMonad[M[_]: Defer, A]: Monad[ContT[M, A, ?]] = |
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.
Random thought…
What if we implicitly prioritize a bit here? ContT
is still useful with very short bind chains even when the underlying monad is not Defer
, it just wouldn't be stack-safe. So make the higher priority Monad
instance a Monad with Defer
and require the Defer[M]
constraint. Provide a lower priority instance which doesn't require or provide the Defer[M]
(and thus also has a stack-unsafe tailRecM
). Obviously this means removing the flatMap
and map
implementations from the ContT
class itself and only making them visible via implicit enrichment (if the imports are a problem for usability, we can with MonadSyntax
on the companion object).
The advantage would be a more broadly applicable ContT
(admittedly, I'm not sure how much people care about continuations on the JVM outside of async effects, so… maybe not a big advantage?) which transparently preserves stack-safety whenever possible, and gracefully degrades whenever not. Maybe this is taking the "tailRecM
doesn't really need to be stack-safe to be lawful" idea too far though.
object ContT { | ||
|
||
// Note, we only have two instances of ContT in order to be gentle on the JVM JIT | ||
// which treats classes with more than two subclasses differently |
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.
You can just make ContT
take runAndThen
as a parameter to get the same benefit without the subtype limit. :-) A more impactful micro-optimization would be to make the implementations within ContT
marked as final
, since then it won't matter how many subtypes you have.
apply { cb => cb(b) } | ||
|
||
def apply[M[_], A, B](fn: (B => M[A]) => M[A]): ContT[M, A, B] = | ||
FromFn(AndThen(fn)) |
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'd also like to see an applyPure
or something like that defined as:
def applyPure[M[_]: Applicative, A, B](fn: (B => A) => A): ContT[M, A, B] =
apply[M, A, B](_(fn.andThen(_.pure)).pure)
Or thereabouts…
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.
actually, I don't think we can do that I think we need Applicative and Comonad (we need to go B => M[A]
to B => A
to call fn
.
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.
Oh I see what you mean. That's definitely unfortunate. I'm not sure the function would be useful at all requiring Comonad
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.
This one's largely above my head, but given that, I went looking for the documentation to figure it out and didn't find any. Probably would be good to document the motivating use case and how to use it before merging.
I'd also be interested in the motivation for this. I've read about Cont and ContT a few times, but I've never seen them used in the wild. As someone who doesn't have much intuition for them, in the use cases that people describe it seems to me like a pull- based stream API like fs2 would be a cleaner solution. But I'm definitely open to learning more about this approach. Do you have a current use-case for this? |
I mostly did it to show we can get a safe tailRecM. Whatever. I’ll let y’all close it if you don’t want it. Not here to try to change minds about it. |
@iravid has an interesting proof of concept that shows an akka-http-like DSL expressed with |
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 am not very familiar with the subject matter, but the code looks sane. Since the API surface is small and it's a data structure rather than typeclass, I vote that we merge and let it out into the wild to test.
@djspiewak is there anything else you would like to be addressed in this PR? |
TLDR; there are many :) People do mind blowing things with ContT. Usually they write interpreters, games, web servers, streaming libraries:
Last one is special ❤️ to me - That is how I got hooked by FP. Long before I learned Scala. When ContT lands in Cats, I will revisit my plan to write optimizing Lisp compiler using CPS in Scala :) There are more sophisticated applications as well: Is there a real-world applicability for the continuation monad outside of academic use? And it is good to have this implemented in library, with PR that passed review 👀 like this one. Otherwise everyone will need to write it by himself. |
@kailuowang Sorry missed this. Nothing more. I'm 👍 |
Thanks @djspiewak! I am merging. |
a stack safe |
Sorry if being lazy but how to best compare |
ContT is a challenging Monad to implement if you need to implement tailRecM.
The addition of the Defer typeclass solves this problem. This PR replaces the unmerged #1400.
cc @non