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

cats.mtl.Local[IO, E] from IOLocal[E] #3429

Draft
wants to merge 3 commits into
base: series/3.x
Choose a base branch
from
Draft

cats.mtl.Local[IO, E] from IOLocal[E] #3429

wants to merge 3 commits into from

Conversation

rossabaker
Copy link
Member

cats.mtl.Local's semantics are a safe and useful subset of what can be done with IOLocal. This provides easy access to a Local[F, E] instance from an IOLocal[E] value.

  • core takes on a cats-mtl dependency. Note that testkit already couples these release cycles.
  • Adds IOLocal#asLocal to get a Local view of an IOLocal value
  • Adds IOLocal.local to create an IOLocal and return a local view of it, inside IO.

A similar view is already in use in otel4s.

Fixes #3385.

Copy link
Member Author

@rossabaker rossabaker left a comment

Choose a reason for hiding this comment

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

Needs docs and such, but I wanted to float the interface first.

@@ -319,4 +332,6 @@ object IOLocal {
underlying.get.flatMap(s => underlying.reset.as(getter(s)))
}

def local[E](e: E): IO[Local[IO, E]] =
Copy link
Member Author

Choose a reason for hiding this comment

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

otel4s has integrated LiftIO, but I'm not sure this is necessary: cats.mtl already lifts a Local[F, E] to all the common transformers over F. Everyone would pay the LiftIO tax just to cover any LiftIO instances not covered by Local.

Comment on lines +256 to +257
def local[B](iob: IO[B])(f: A => A): IO[B] =
self.modify(e => f(e) -> e).bracket(Function.const(iob))(self.set)
Copy link
Member Author

Choose a reason for hiding this comment

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

This could conceivably be made a method on IOLocal itself as well, with this becoming a simple delegate.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like thin instances, but if we're discouraging non-Local use of IOLocal, maybe we shouldn't make it easier to use.

@@ -319,4 +332,6 @@ object IOLocal {
underlying.get.flatMap(s => underlying.reset.as(getter(s)))
}

def local[E](e: E): IO[Local[IO, E]] =
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't love the name.

Comment on lines 32 to 38
implicit val local: Local[IO, Int] =
// Don't try this at home
unsafeRun(IOLocal.local(0)).fold(
throw new CancellationException("canceled"),
throw _,
_.get
)
Copy link
Member

Choose a reason for hiding this comment

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

We can also use syncStep here.

Copy link
Member Author

Choose a reason for hiding this comment

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

The left seems as messy as this, but I might be missing something.

Comment on lines 335 to 336
def local[E](e: E): IO[Local[IO, E]] =
IOLocal(e).map(_.toLocal)
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be interesting to see this as a method on IO so you can do IO.local(e): IO[Local[IO, E]]. It also insulates the API from the more complex stuff going on in IOLocal—thinking from the perspective of users.

Copy link
Member Author

@rossabaker rossabaker Feb 17, 2023

Choose a reason for hiding this comment

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

That's exactly where I wanted to put it, but it creates a cycle between IO and IOLocal that isn't already there. I don't think I mind: I can't imagine those ever not being in the same module.

Copy link
Member

Choose a reason for hiding this comment

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

They'll definitely always be in the same module. There's already an indirect cycle (through the IO case class structure and IOFiber) due to the implementation, so I don't think there's any real concern about making the ABI directly cyclic.

@armanbilge armanbilge added this to the v3.6.0 milestone Feb 17, 2023
@djspiewak
Copy link
Member

core takes on a cats-mtl dependency. Note that testkit already couples these release cycles.

This is the one thing that gives me pause. I'm concerned about it mostly because I suspect there are breaking changes we will want to make to Cats MTL in the near-ish term, which transitively breaks Cats Effect. However, as you pointed out, testkit already implies this version dependency, which is unfortunate but definitely undercuts my concern.

@djspiewak
Copy link
Member

Particularly with an eye towards otel4s, are we sure we want to privilege bare IOLocal[A] in this way? My concern is that this ossifies a very particular propagation semantic rather than encouraging users to be more selective about what semantics they want.

@rossabaker
Copy link
Member Author

I don't see that otel4s privileging bare IOLocal. It's saying context propagation should be done with cats.mtl.Local semantics, because advanced but common uses of resource and race do weird shit. I fixed the FS2 problems under stateful propagation with blunt force more than thoughtful reason, but it still wasn't satisfactory.

I do want to be cautious here, because if cats-mtl is not stable, this PR makes everything unstable. But we hope to integrate otel4s into the major networking libraries, and without cats-mtl, we're missing a typeclass and then really are privileging IOLocal.

My two questions:

  1. If you were designing IOLocal today, what interface would you want beyond the operations of cats.mtl.Local? Remember the IOLocal can hold a Ref for cases children need to merge state back into the parent.

  2. (Perhaps a better issue for cats-mtl) After a long period of stability, what changes do you imagine in cats-mtl? If cats-mtl-2 is coming, that changes a lot of plans for a lot of projects.

@djspiewak
Copy link
Member

After a long period of stability, what changes do you imagine in cats-mtl?

I imagine very little changing in the typeclasses. I'm mostly interested in smoothing the introduction/elimination of capabilities, particularly those with lexical scopes (i.e. all the interesting ones). That's probably something that can be done in a binary-compatible fashion, but I don't know for sure until I really go deep on it.

@rossabaker
Copy link
Member Author

otel4s could kick this can down the road by not depending on cats-mtl, restricting itself to IOLocal operations representing local semantics, and either removing support or massively duplicating code for Kleisli and all transformers. natchez-core is an approximation of what that road looks like, and it would be nice not to repeat that aspect of it.

When you say "ossifies a very particular propagation semantic," otel4s is pretty much going to have to do so with respect to tracing. We can have a tracer that uses local semantics, which sucks for spanning Resource#use and is good for everything else. Or we can span with a Resource, which gives a lovely API as long everybody uses the orthodox resource interpreter, but corrupts when they don't. Perhaps there's something behind door number three, but even then, we'll need http4s and skunk and friends to pick the same door.

@rossabaker
Copy link
Member Author

Note that there is also lawful Stateful instance, which does weird shit with concurrency between .set and .get. Since it's lawful, it's not better nor worse than public .set. It's milquetoast to neither provide that instance nor push for deprecation of IOLocal's stateful bits, but here I sit... for now. I would love to see a practical example that can best be done as a Stateful view of IOLocal.

@kubukoz
Copy link
Member

kubukoz commented Sep 11, 2024

Note that there is also lawful Stateful instance, which does weird shit with concurrency between .set and .get.

I'm not sure I get the problem - one fiber's view of an IOLocal can't be changed by someone else between set and get, can it? We don't propagate updates from child fibers to the parent nor from the parent to the children, afaik, so each fiber (a unit of concurrent execution) would be immune to weird shit from others... unless you meant something else.

@djspiewak
Copy link
Member

Can we get a non-draft version of this?

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.

IOLocal: MTL Local instance & semantics
5 participants