Cats-effect: Provide a way to `unroll` up to asynchronous boundary of IO

Created on 27 Dec 2017  Â·  14Comments  Â·  Source: typelevel/cats-effect

I would like to propose a feature to have ability to unroll synchronous part of F execution steps, that will run all synchronous code within the F up to asynchronous boundary.

Not sure if given laws etc it shall be on Sync/Async interface, but essentially I would like to have something simple as this (not sure 100% about naming)

def syncView(f: F[A]): Either[F[A], A] 

where this will return on left F[A] that contains F with asynchronous execution steps, whereas on right, if F was composed purely from synchronous steps.

This may greatly help optimize code that implements interruption, which won't be necessary for synchronous F but is necessary for asynchronous steps and is achieved via racing results from two F (one with result other with interruption signal).

Most helpful comment

@pchlupacek how about a signature that returns IO?

def runSyncMaybe[A](f: F[A]): IO[Either[F[A], A]]

It's consistent with Effect#runAsync and the good thing about it is that we can guarantee, by law, that we can do unsafeRunSync on it, which would be useful for optimizations that involve eliminating unnecessary async boundaries.

All 14 comments

If it were on Async, it would need to be def syncView(f: F[A]): F[Either[F[A], A]]. I'm pretty strongly :-1: on putting unsafe functions into typeclasses, which is exactly what your original type signature would be. I'm not sure whether or not the safe version has the same optimization potential.

The other problem with this is it's actually remarkably close to a foldMap into something like Free[F, A]. We don't have foldMap defined, but we do have to on IO (which is literally foldMap, but for Async) and it isn't hard to define Async[Free[F, ?]] given Async[F]. So in other words, if your F is an IO, you can already achieve this pretty directly. We don't have to on Effect, but it wouldn't be hard to do so.

@djspiewak I am ok with F[...] signature. However rather than on Async, I would like to put that on Sync, if possible. I think is completely ok for Sync F to alway return F[Right[A] whereas for F that do have Async available to return on left.

I think implementation of this shall not be complicated, is just more to decide on signature and where to put it.

I am not sure if I am following your reference to foldMap.

@djspiewak could we agree on something like this on Sync:

/** runs `fa` up to synchronous boundary, either returning result or asynchronous continuation of `f` **/
def runSync[A](fa: F[A]):IO[Either[F[A], A]] = ???

Generally no problem if IO will be replaced by F, but was sort of inspired by runAsync on Effect

more thinking about it rather F instead of IO ....

I generally like the function, and I think specializing it to IO (like runAsync) makes some sense, parametrically. Looping back to the foldMap reference though, which is what makes me reticent to specialize on something like this when a more general form may exist…

IO is two free monads glued together: one over a trivial algebra, the other over its CPS duel (which is non-algebraic). Any algebraic monad can define a foldMap given a natural transformation which maps its algebra into some other monad, which it then reduces (with flatMap); this is basically just category theory. IO cannot define this directly, since half of its algebra is… non-algebraic. However, it can define it for the subset of its algebra which is algebraic, provided it can encode a stop case. Something like this, for example:

type Alg[A] = Either[Throwable, () => A]

def foldMap[M[_]: Monad, A](ioa: IO[A])(nt: Alg ~> M): EitherT[M, IO[A], A]

As you can see, this looks remarkably like your function. :-) So you see why I'm hesitating and thinking through ways of generalizing it.

Bump -- FS2 team would like to get this in to cats-effect 1.0 in order to implement functional-streams-for-scala/fs2#1058

Guys, this isn't an easy issue to solve. So I'm seeing two cases: One would be:

def syncView(f: F[A]): Either[F[A], A]

def syncView(f: F[A]): IO[Either[F[A], A]]

This can't be on less than Effect. I also believe this cannot be on Sync:

def syncView(f: F[A]): F[Either[F[A], A]]

It's not obvious, but Sync or Async data types do not have the requirement for streaming an A exactly once. The delay and async builders are putting values in the F context, this is the opposite, taking values out of the F context. Which is the purpose of Effect.

Also, I'm not sure if this particular implementation helps you. For example the returned F[Either[F[A], A]] might not have synchronous execution on evaluation. It's only IO that can provide that guarantee, due to its current implementation.

So why Sync?

@alexandru thanks a lot to looking into this. I think naming and idea came up before recent changes in IO and type class hierarchy. So lets sort of update it :

What we need is for the streams, that are subject to asynchronous nondeterministic interruption to implement that interruption on almost every frame of F evaluation of running stream. _Almost_ in this context means we don't want to orchestrate interruption for F that is known to be not cancellable.

Essentially we need to somehow inspect F[A] and if that is not cancellable (like now, suspend...) we want just await its execution, before moving forward. if F[A] is interruptible, we want to race with cancelation signal and possibly cancel the F[A] if the interruption was signalled.

Idea of Sync was just to have it on lowest possible type class where I thought it is feasible to implement. In interruption currently we require Effect so if Effect is required, no problem with that.

I think with new implementation we may require Concurrent or ConcurrentEffect instead of Effect so that functionality may be provided again on lowest possible type denominator where this may be implemented.

@pchlupacek so the implementation for IO already exists and used internally. I felt the need for it as well.

But I'm not sure if we can provide this on anything lower than Effect. Will look into it.

@alexandru thanks a lot. Effect is fine.

@pchlupacek how about a signature that returns IO?

def runSyncMaybe[A](f: F[A]): IO[Either[F[A], A]]

It's consistent with Effect#runAsync and the good thing about it is that we can guarantee, by law, that we can do unsafeRunSync on it, which would be useful for optimizations that involve eliminating unnecessary async boundaries.

Any volunteers to finish this? It seems like there is a general consensus on the approach. It would be great to get this done and keep the 1.0 release on track.

@rossabaker I can attempt to do this. I'll let you know how it goes.

I've sent a PR: #195.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tpolecat picture tpolecat  Â·  5Comments

djspiewak picture djspiewak  Â·  4Comments

Odomontois picture Odomontois  Â·  8Comments

Daenyth picture Daenyth  Â·  3Comments

Daenyth picture Daenyth  Â·  3Comments