Comfy Cats Effect room. Ask questions. Rant about config file formats. Be nice!
mapN
, a useN
if you will?
Semigroupal
and its product
operation
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
tupled
abstracts over the amity of product
in the same way mapN
abstract over the arity of ap
ap
and pure
or in terms of unit: F[Unit]
and product
F.raiseError
should be fine
Sorry, I need a little more help, since I want my result to be F[Option[User]]
, I'm not sure how to mix in the raise error:
res.status match {
case StatusCodes.OK => Option(res.entity)
case StatusCodes.NotFound => None
case _ => Async[F].raiseError(new Exception(""))
}
This changes the result to Any
which makes sense but I'm not sure how to get around it
pure
is the way to go. Note that you also can use a less powerful type class to raise the error: ApplicativeError[F, Throwable].raiseError(new Exception(""))
. Doesn’t change anything per se (and hence isn’t too important), but keeps your code at the level of power it needs and not more, so consider it a good practice.
Thank you for the feedback @ybasket , I saw ApplicativeError as you wrote in the docs but struggled to get something compiling. The end result is kind of disgusting with all the type hints...
res.status match {
case StatusCodes.OK => Async[F].pure(Option(res.entity))
case StatusCodes.NotFound => Async[F].pure(Option.empty[ResponseEntity])
case _ => Async[F].raiseError[Option[ResponseEntity]](new Exception(""))
}
But it compiles! And that's what's important
@slouc Attempting to answer your questions!
Async#shift(ec) (in companion object) will be replaced by Async#evalOn(f, ec) (in the typeclass)
Broadly, yes. shift
right now just tosses the continuation of the computation onto the given thread pool, without any guarantees about anything. evalOn
is a lot more constrained in that you hand it an effect wherein all actions will be moved to the given pool (similar to the continuation of today's shift
), and then once that effect is finished you revert back to whatever your default is (which may in turn be set by some evalOn
which is wrapped around you).
One way to think about this evalOn(evalOn(fa, ec1), ec2)
has the semantics you would expect. You can't replicate that with `shift.
Also note that doing things in this fashion allows us to implement the executionContext: F[ExecutionContext]
effect on Async
, which gives you the ability to get access to the ExecutionContext
which is governing your scheduling. This is super-important for interoperating with non-cats-effect APIs that still need a raw context (most notably Future
).
ContextShift#shift will be replaced by Concurrent#cede (naming is still WIP)
cede
is actually a little more general than shift
. It doesn't take a executor, obviously, and is really more about yielding control back to the scheduler. It is the fundamental operation of parallelism in a cooperative multitasking environment. In a purely cooperative system (such as coop), if you never cede
, then you get no parallelism and one fiber will hog all the resources.
Now, in CE2, shift
is the only real way we have of declaring a yield, so it kind of serves double-duty in that sense: by shift
ing back to the pool you're already assigned to, you yield control and allow other tasks to run. cede
will do this in many implementations, except yielding back to the pool which is already in the reader environment (which is why it doesn't need to be specified). Critically, cede
is also lawfully compatible with implementations that don't use ExecutionContext
at all! (such as coop)
cede
is also a hint. It's not strictly required to do anything. So implementing datatypes such as Monix might ignore a cede
if it comes immediately after an auto-yield boundary, for example.
Fundamentally, it's about fairness. Whenever you encode a long-running loop within F
, you should probably toss a cede
in there every so often in order to ensure that other fibers get their turn. How often you do this determines how much throughput (i.e. how quickly you compute the response for a single request) you want to sacrifice to achieve better fairness (i.e. how long it takes you to respond to concurrent requests).
I'm not sure about
IO#shift(ec)
andIO#shift(cs)
, but I'm assuming that they will be removed as well, and their role will be fulfilled byAsync[IO]#evalOn(f, ec)
Same deal! There will probably be an IO.evalOn
just for convenience, but it'll do the same thing as Async[IO].evalOn
n
steps.Would you agree with this viewpoint, and do you think cats-effect should retain its philosophy?
Strongly agree with that viewpoint. This is basically how I think about it as well.
As for whether or not cats-effect IO
should retain that mode of operation… I'm honestly not sure. I kind of like the fact that it fills the niche at the "throughput by default" end of the spectrum, since the rest of the spectrum is well covered by the other options. However, the lack of auto-yielding has bitten me (though very, very very rarely for the reasons that @SystemFw pointed out: most code is fairer than you think it is).
I think there's room for discussion on this point for sure.
Question: given a factory method that effectfully creates an object in, say, F
, and the created object is also created from a class parameterized over an effect type, do I need to do something like:
def create[F, G](implicit F: ...)(...): F[Thing[G]]
or do you all normally just leave everything as F
and be done with it assuming all effect types converge to IO
?
I share that opinion. But, I'm wondering if there are cases where you have a lot of flatmapped actions stacked up on each other in a gigantic IO program, and you know you would like to yield every once in a while, but it's hard to inject that into your code because it's not really clear where those points actually are.
That's the concern exactly. In practice I've found this is rare to the point of non-existent, but I imagine it could happen. The only times I've been bitten by lack of fairness, it was my own fault and relatively easy to fix. If you think about what has to happen for a long series of CPU-hogging code to avoid any async boundaries at all, it usually requires a ton of pure compute code (so, you're doing map
with some f
which is expensive but pure). In that case, it doesn't matter whether your semantics are auto-yielding or not: it's going to hog the thread.
For there to be code which can be more fairness-optimized by some mechanism in the effect type, but isn't already so optimized, you need a ton of delay
actions bound together with flatMap
s. A ton of them. Without any async
in between and without any shift
s to other pools. That… happens… very very very rarely.
Which is to say that auto-yielding isn't as helpful as it sounds in practice. Certainly still meaningful, but more meaningful on paper than in reality.
If I use Async.async() inside of Resource.use(), I need to ensure myself that somehow the async operation is completed prior to the resource's release method is called
The use
method will not be called until (at least!) the callback inside of async
is run. Which is to say, you have control over it. Remember that async
doesn't mean "parallel", it just means non-blocking.
R
. Within the use
method I use Async.async() on R
(so the callback necessarily has to be called after the use
method). Assuming I am not altering ExecutionContexts or ContextShifts, is it possible for the release
method to be called prior to the callback finishing if I do not explicitly synchronize this?
use
, not after