scoped

Use case

Scoped uses IOLocal to share context through an application as documented here

(from the documentation)

                      ┌────────────┐               ┌────────────┐
                 fork │  Fiber B   │ update(_ + 1) │  Fiber B   │
               ┌─────►│ (local 42) │──────────────►│ (local 43) │
               │      └────────────┘               └────────────┘
┌────────────┐─┘                                   ┌────────────┐
│  Fiber A   │        update(_ - 1)                │  Fiber A   │
│ (local 42) │────────────────────────────────────►│ (local 41) │
└────────────┘─┐                                   └────────────┘
               │      ┌────────────┐               ┌────────────┐
               │ fork │  Fiber C   │ update(_ + 2) │  Fiber C   │
               └─────►│ (local 42) │──────────────►│ (local 44) │
                      └────────────┘               └────────────┘

One example use case for this is passing around transaction ids where you can set them at the start and any further effects are able to access.

Example

Lets start by creating a datatype we want to be shared around

opaque type TransactionId = String
object TransactionId:
  def fromString(str: String): TransactionId = str

We can wrap it up and give it some useful name

import is.ashley.scoped.Scoped 

type Transactional[F[_]] = Scoped[F, TransactionId]

object Transactional:
  def apply[F[_]](using ev: Transactional[F]): Transactional[F] = ev

And now a service that will be using our new datatype, here we can use Transactional as a constraint on F[_] ensuring that we have the value set in scope.

class MyService[F[_]: Transactional]:
  def doSomething: IO[Unit] = 
    for 
      txid <- Scoped[F, TransactionId].get // txid = "not-set"
      _ <- Transactional[F].scope("123").use(new MyOtherService)
    yield ()

Another service using Transactional this service is called by the .use above and has had the TransactionId value changed.

class MyOtherService[F[_]: Transactional]:
  def doOtherThing: IO[Unit] = 
    for 
      txid <- Transactional[F].get // txid = "123"
      ...
    yield ()

In order to setup the scoping we create a TransactionId here and hand it to Scoped.fromIOLocal we can then use this to meet our Transactional constraint further down the stack.

Scoped.fromIOLocal(TransactionId.fromString("not-set")).flatMap { 
  implicit transactionalScope: Transactional[IO] =>
    val service = new MyService[IO]
    service.doSomething
}