Haskell has a function for getting the current continuation
getCC = callCC (\c -> let x = c x in return x)
How to write a similar function in Scala?
E.g. function callCC presents in cats.ContT. How could we use it?
I've tried many ways, but I can't make ends meet..
Let's implement this
getCC("get current continuation") in Scala.For starters, let's understand what is actually happening in Haskell. When we look at the docs and sources we'll find:
So what we see here:
we are defining a
newtype, a way of using an existing type, giving it a new name and then hiding from the rest of the code the actual type, so that all interaction with that type will go only through our API - Scala 3's counterpart would beopaque type, but in the past it was often done with wrappers extending AnyVal, or sometimes plain wrappers. Cats' decided to use a plain class:but we are not defining all that continuation magic for
ContTtype only, no, we are creating a separate type classMonadContwhich defines functioncallCC, and you could provide an instance for various different types.ContTis just one of them. If we wanted to express it in Scala 3, it would be:in Cats however things are a bit simpler, so
callCCis defined directly inContTcompanion object, and it is defined only forContTso it looks like this:just by comparing the code we can see, that when Haskell code uses generic
min context ofContT[M, R, A]it is NOT justMbut WHOLEContT[M, R, *]Ok, so let's move to
getCCpart:getCCis defined in Haskell more or less asthat
m -> m (m a)is important but only with the knowledge thatmisContT[M, R, *]we can actually decode the type signature, and what it would look like in Scala:now we must use types to guide our implementation. We'll start with some stubs:
but we know what our
resulttype has to be -ContT[M, R, ContT[M, R, A]]- so what values ofXandZwould make it type check?let's substitute
XandYand eliminate type aliases:before we figure out
Zwe need to stop for a moment. The implementation code was:We have to be reminded that in Haskell
returnis a function fromMonadtypeclass used to... wrap the value returned indocomprehension. In other word it's what Cats knows asdef puremethod in theMonadtype class. So that lets us adjust the code to this:further analysis of types lets us figure out the types that needs to be passed into
pureto get the expectedresult- in particular what isx:so far, so good, now we only need to figure out how
xcomes to be and what isZ. The hint is in Haskell definition:let x = c x. Lets try to write it in Scala:putting compiler warnings aside (using
val xin its own definition is not allowed) this code would make sense ifcmethod returnedContT[M, R, A]]... but it returnsContT[M, R, Z]]... soZ = Anow we only have to solve the last issue - make
val x = c(x)compile. The problem comes from an eager evaluation - in Haskell everything is lazy, so there is no such issue that when we evaluatexwe are immediately callingc(x), but to evaluatec(x)we need to know the value ofx, etc. It's one of the reasons whyContTusesDefer[M]a lot - to introduce that lazy evaluation which would let you create the value without a circular dependency in its initialization. So, lets just use some way of deferring the content ofxbut still creating a reference tox. One such way I found was throughlazy valandContT.later(which happens to take as by-name param, what.runis returning):Then you can make this method an extension method on
ContTcompanion object, e.g. for Scala 3 it would beand as you already verified it works:
I would not be completely sure it always works - I'd suggest putting a lot of tests precisely because we have to manually address this eager vs lazy value problem ourselves, but it should pretty much explain the idea.
Notice, how much effort went into manually resolving all kind of type parameters. (And in understanding what is going on in general, it's pretty counterintuitive, and I have to learn this style anew every time I meet it again.) In Haskell type resolution works in a different way and it allows to just skip it (what other issues it brings I'll leave to Haskellers to explain). But it is definitely opaque to read, hard to debug, and difficult to maintain. I may see its use case in some internal logic that only a few people have to tinker with (and only if it actually brings some value!), but I'd definitely recommend against using it commonly in codebase and in business logic.