zio.blocks.scope provides compile-time verified resource safety for synchronous code by tagging values with an unnameable, type-level scope identity. Values allocated in a scope can only be used when you hold a compatible Scope, and values allocated in a child scope cannot be returned to the parent in a usable form.
Structured scopes. Scopes follow the structured-concurrency philosophy: child scopes are nested within parent scopes, resources are tied to the lifetime of the scope that allocated them, and cleanup happens deterministically when the scope exits (finalizers run LIFO). This "nesting = lifetime" structure provides clear ownership boundaries in addition to compile-time leak prevention.
If you've used try/finally, Using, or ZIO Scope, this library lives in the same problem space, but it focuses on:
- Compile-time prevention of scope leaks
- Zero-cost opaque type (
$[A]is the scoped type, equal toAat runtime) - Simple, synchronous lifecycle management (finalizers run LIFO on scope close)
- Eager allocation (all scope operations execute immediately;
Resourceis a lazy description that becomes eager when passed toallocate)
- Quick start
- Core concepts
- Safety model (why leaking is prevented)
- Usage examples
- Allocating and using a resource
- Nested scopes (child can use parent, not vice versa)
- Chaining resource acquisition
- Registering cleanup manually with
defer - Classes with
Finalizerparameters - Classes with
Scopeparameters (scope injection) - Non-lexical scopes with
open() - Dependency injection with
Wire+Context - Dependency injection with
Resource.from[T](wires*) - Injecting traits via subtype wires
- Interop escape hatch:
leak
- Common compile errors
- API reference (selected)
import zio.blocks.scope._
final class Database extends AutoCloseable {
def query(sql: String): String = s"result: $sql"
def close(): Unit = println("db closed")
}
Scope.global.scoped { scope =>
import scope._
val db: $[Database] = allocate(Resource(new Database))
// scope.use applies a function to the scoped value, returning a scoped result
val result: $[String] = scope.use(db)(_.query("SELECT 1"))
println(result) // $[String] = String at runtime, prints directly
}Key things to notice:
allocate(...)returns a scoped value of type$[Database](the path-dependent type of the enclosing scope)$[A] = Aat runtime — zero-cost opaque type, no boxing- All operations are eager — values are computed immediately, no lazy thunks
- Use
scope.use(value)(f)to work with scoped values; returns$[B] - When the
scoped { ... }block exits, finalizers run LIFO and errors are handled safely - The
scopedmethod requiresUnscoped[A]evidence on the return type
Scope is a sealed abstract class with no type parameters. It manages finalizers and ties values to a type-level identity via abstract type members.
type $[+A]— a path-dependent opaque type that tags values to this scope. Covariant inA. Equal toAat runtime (zero-cost).type Parent <: Scope— the parent scope's type.val parent: Parent— reference to the parent scope.
Each scope instance exposes its own $[+A], so a parent's $[Database] is a different type than a child's $[Database], even though both equal Database at runtime.
type $[+A] // = A at runtime (zero-cost)So in code you'll typically write:
Scope.global.scoped { scope =>
import scope._
val x: $[Something] = ??? // or scope.$[Something]
}Child scopes are represented by Scope.Child[P <: Scope], a final class nested in the Scope companion object.
Scope.global is the root of the scope hierarchy:
object Scope {
object global extends Scope {
type $[+A] = A
type Parent = global.type
val parent: Parent = this
}
}- The global scope is intended to live for the lifetime of the process.
- On the JVM, its finalizers run on shutdown via a shutdown hook. On Scala.js, no shutdown hook is available, so global finalizers do not run automatically.
$[+A] (or scope.$[A] in type annotations) is a path-dependent opaque type representing a value of type A that is locked to a specific scope. It is covariant in A.
- Runtime representation:
$[A] = A— zero-cost opaque type, no boxing or wrapping - Key effect: methods on
Aare hidden at the type level; you can't calla.methoddirectly - All operations are eager:
allocate(resource)acquires the resource immediately and returns a scoped value - Access paths:
scope.use(a)(f)to apply a function and get$[B]
Scope provides an implicit class ScopedOps[A] that adds map and flatMap to $[A] values, enabling for-comprehension syntax:
Scope.global.scoped { scope =>
import scope._
val x: $[Int] = $(42)
val y: $[String] = x.map(_.toString)
val z: $[String] = x.flatMap(v => $(s"value: $v"))
}sa.map(f: A => B): $[B]— appliesfto the unwrapped value, re-wraps the resultsa.flatMap(f: A => $[B]): $[B]— appliesfto the unwrapped value (wherefreturns a scoped value)- All operations are eager (zero-cost)
In Scala 2, the scoped method must be called with a lambda literal. Passing a variable or method reference is not supported due to macro limitations:
// ✅ OK: lambda literal
Scope.global.scoped { scope => ... }
// ❌ ERROR in Scala 2 (works in Scala 3):
val f: Scope.Child[_] => Any = scope => ...
Scope.global.scoped(f)Resource[A] describes how to acquire an A and how to release it when a scope closes. It is intentionally lazy: you describe what to do, and allocation happens only through:
allocate(resource)Common constructors:
Resource(a)- Wraps a by-name value; if it's
AutoCloseable,close()is registered automatically.
- Wraps a by-name value; if it's
Resource.acquireRelease(acquire)(release)- Explicit lifecycle.
Resource.fromAutoCloseable(thunk)- A type-safe helper for
AutoCloseable.
- A type-safe helper for
Resource.from[T](wires*)(macro)- The primary entry point for dependency injection.
- Resolves
Tand all its dependencies into a singleResource[T]. - Auto-creates missing wires using
Wire.sharedfor concrete classes. - Requires explicit wires for: primitives, functions, collections, and abstract types.
- If
Tor any dependency isAutoCloseable, registersclose()automatically.
Resource has two important internal flavors:
Resource.Unique[A]- Produces a fresh instance every time you allocate it (typical for
Resource(...),acquireRelease, etc.).
- Produces a fresh instance every time you allocate it (typical for
Resource.Shared[A]- Produces a shared instance per
Resource.Sharedvalue, with reference counting:- the first allocation initializes the value using a child scope parented to
Scope.global - each allocating scope registers a decrement finalizer
- when the reference count reaches zero, the shared scope is closed (running its finalizers)
- the first allocation initializes the value using a child scope parented to
- Produces a shared instance per
Important clarification: sharing is not "memoized within a Wire graph" or "within a scope" by magic. Sharing happens within the specific Resource.Shared instance you reuse.
The Unscoped[A] typeclass marks types as pure data that don't hold resources. The scoped method requires Unscoped[A] evidence on the return type to ensure only safe values can exit a scope.
Built-in Unscoped types:
- Primitives:
Int,Long,Boolean,Double, etc. String,Unit,Nothing- Collections of Unscoped types
Custom Unscoped types:
// Scala 3:
case class Config(debug: Boolean)
object Config {
given Unscoped[Config] = Unscoped.derived
}
// Scala 2:
case class Config(debug: Boolean)
object Config {
implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
}Allowed return types from scoped:
Unscopedtypes: Pure data that can safely exitNothing: For blocks that throw
Rejected return types (no Unscoped instance):
- Closures:
() => Acould capture the child scope - Scoped values:
$[A]would be use-after-close - The scope itself: Would allow operations after close
Scope.global.scoped { parent =>
import parent._
// ✅ OK: String is Unscoped
val result: String = scoped { child =>
"hello"
}
// ❌ COMPILE ERROR: $[Database] has no Unscoped instance
// val escaped = scoped { child =>
// import child._
// allocate(Resource(new Database)) // $[Database] can't escape
// }
}When working in a child scope, you may need to access values allocated in a parent scope. Use lower(parentValue) to "lower" a parent-scoped value into the child scope:
Scope.global.scoped { parent =>
import parent._
val parentDb: $[Database] = allocate(Resource(new Database))
scoped { child =>
import child._
// Use lower() to access parent-scoped value in child scope
val db: $[Database] = lower(parentDb)
child.use(db)(_.query("SELECT 1"))
"done"
}
}The lower operation is necessary because each scope has its own $[A] opaque type. A parent's $[A] is a different type than a child's $[A], even though both equal A at runtime.
Wire is a recipe for constructing services. It describes how to build a service given its dependencies, but does not resolve those dependencies itself.
Inis the required dependencies (provided as aContext[In])Outis the produced service
There are two wire flavors:
Wire.Shared: produces a shared (memoized) instanceWire.Unique: produces a fresh instance each time
Important clarification: Wire itself is just a recipe. The sharing/uniqueness behavior is realized when the wire is used inside Resource.from, which composes Resource.Shared or Resource.Unique instances accordingly. Sharing is per Resource.Shared instance, not per-scope or per-graph: if wire.toResource(ctx) is called twice, you get two independent ref-counted singletons that don't share with each other. Inside Resource.from, each wire produces exactly one Resource, so diamond dependencies correctly share a single instance.
There are exactly 3 DI macro entry points:
| Macro | Purpose |
|---|---|
Wire.shared[T] |
Create a shared wire from T's constructor |
Wire.unique[T] |
Create a unique wire from T's constructor |
Resource.from[T](wires*) |
Wire up T and all dependencies into a Resource |
For wrapping pre-existing values:
Wire(value)— wraps a value; ifAutoCloseable, registersclose()automatically
- Collect wires: Uses explicit wires when provided, otherwise auto-creates with
Wire.shared - Validate: Checks for cycles, unmakeable types, duplicate providers
- Topological sort: Orders dependencies so leaves are allocated first
- Generate composition: Produces a
Resource[T]via flatMap chains
Key insight: Compose Resources, don't accumulate values. Each wire becomes a Resource, and they are composed via flatMap. This correctly preserves:
- Sharing: Same
Resource.Sharedinstance → same value (even in diamond patterns) - Uniqueness:
Resource.Unique→ fresh value per injection site
When Resource.from needs a dependency of type Service, it will accept a wire whose output is a subtype (e.g., Wire.shared[LiveService] where LiveService extends Service). This enables trait injection without extra boilerplate.
If the same concrete wire satisfies multiple types (e.g., Service and LiveService), only one instance is created and reused for both.
Pragmatic safety. The type-level tagging prevents accidental scope misuse in normal code, but it is not a security boundary. A determined developer can bypass it via leak (which emits a compiler warning), unsafe casts (asInstanceOf), or storing scoped references in mutable state (var). The guarantees are "good enough" to catch mistakes in regular usage, not protection against intentional circumvention.
The library prevents scope leaks via two reinforcing mechanisms:
Child scopes are created with:
Scope.global.scoped { scope =>
import scope._
scoped { child =>
import child._
// allocate in child
}
}The child scope has a fresh, unnameable $[A] type (created per invocation). You can allocate in the child, but you can't return those values to the parent in a usable form because the parent cannot name (or satisfy) the child's $[A] type.
Compile-time safety is verified in tests, e.g.:
ScopeCompileTimeSafetyScala3Spec.
Each scope defines its own $[A] opaque type. Even though $[A] = A at runtime, the compiler treats each scope's $[A] as distinct. A child's $[Database] is a different type than the parent's $[Database].
Additionally, the opaque type hides A's methods at the type level — you can't call db.query(...) directly on a $[Database]. Access routes are scope.use(value)(f) and the ScopedOps methods (map, flatMap) for for-comprehensions.
import zio.blocks.scope._
final class FileHandle(path: String) extends AutoCloseable {
def readAll(): String = s"contents of $path"
def close(): Unit = println(s"closed $path")
}
Scope.global.scoped { scope =>
import scope._
val h: $[FileHandle] = allocate(Resource(new FileHandle("data.txt")))
// scope.use applies function to scoped value, returns $[String]
val contents: $[String] = scope.use(h)(_.readAll())
println(contents) // $[String] = String at runtime
}import zio.blocks.scope._
Scope.global.scoped { parent =>
import parent._
val parentDb: $[Database] = allocate(Resource(new Database))
scoped { child =>
import child._
// Use lower() to access parent-scoped values in child scope:
val db: $[Database] = lower(parentDb)
println(child.use(db)(_.query("SELECT 1")))
val childDb: $[Database] = allocate(Resource(new Database))
// You can use childDb *inside* the child:
println(child.use(childDb)(_.query("SELECT 2")))
// But you cannot return childDb to the parent:
// $[Database] has no Unscoped instance — compile error
// Return an Unscoped value
"done"
}
// parentDb is still usable here:
println(parent.use(parentDb)(_.query("SELECT 3")))
}Since $[A] supports map and flatMap via ScopedOps, you can chain resource acquisitions in for-comprehensions:
import zio.blocks.scope._
class Pool extends AutoCloseable {
def lease(): Connection = new Connection
def close(): Unit = println("pool closed")
}
class Connection extends AutoCloseable {
def query(sql: String): String = s"result: $sql"
def close(): Unit = println("connection closed")
}
Scope.global.scoped { scope =>
import scope._
// Chain allocations in a for-comprehension:
// flatMap unwraps $[Pool] to Pool, so pool.lease() returns a raw Connection
val result: $[String] = for {
pool <- allocate(Resource.from[Pool])
conn <- allocate(Resource(pool.lease()))
} yield conn.query("SELECT 1")
println(result)
}
// Output: result: SELECT 1
// Then: connection closed, pool closed (LIFO)Use defer when you already have a value and just need to register cleanup. defer returns a DeferHandle that can be used to cancel the finalizer before the scope closes.
import zio.blocks.scope._
Scope.global.scoped { scope =>
import scope._
val handle = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
val deferHandle: DeferHandle = defer { handle.close() }
val firstByte = handle.read()
println(firstByte)
// Optionally cancel the finalizer if you've already cleaned up:
// deferHandle.cancel()
}There is also a package-level helper defer that only requires a Finalizer:
import zio.blocks.scope._
Scope.global.scoped { scope =>
import scope._
given Finalizer = scope
defer { println("cleanup") }
}If your class only needs to register cleanup logic, accept a Finalizer parameter. The wire and resource macros automatically inject the Finalizer when constructing such classes.
import zio.blocks.scope._
class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
private val pool = createPool(config)
finalizer.defer { pool.shutdown() } // or: defer { ... } with import zio.blocks.scope._
def getConnection(): Connection = pool.acquire()
}
// The macro sees the implicit Finalizer and injects it automatically:
val resource = Resource.from[ConnectionPool](Wire(Config("jdbc://localhost")))
Scope.global.scoped { scope =>
import scope._
val pool = allocate(resource)
// pool.shutdown() will be called when scope closes
}When to use Finalizer instead of Scope:
Finalizeris the minimal interface—it only hasdefer- Use it when your class only needs to register cleanup actions
- Since
Scope extends Finalizer, aScopeis always passed at runtime; declaringFinalizersimply narrows the visible API
If your class needs full scope capabilities—creating child scopes, allocating sub-resources, or managing per-request lifetimes—accept a Scope parameter instead of Finalizer. The macros automatically inject the Scope when constructing such classes.
import zio.blocks.scope._
class RequestHandler(config: Config)(implicit scope: Scope) {
// Create child scopes for per-request resource management:
def handle(request: String): String = {
scope.scoped { child =>
import child._
val conn = allocate(Resource(new Connection(config)))
child.use(conn)(_.query(request))
}
}
}
// The macro sees the implicit Scope and injects it automatically:
val resource = Resource.from[RequestHandler](Wire(Config("jdbc://localhost")))
Scope.global.scoped { scope =>
import scope._
val handler = allocate(resource)
scope.use(handler)(_.handle("SELECT 1"))
}The Scope parameter can appear in any parameter list position—value, implicit, or using (Scala 3). The macros detect Scope parameters the same way they detect Finalizer parameters.
When to use Scope instead of Finalizer:
- The class needs to create child scopes (via
scopedoropen()) - The class needs to allocate sub-resources (via
allocate) - The class manages per-request or per-operation lifetimes
The scoped method creates a lexically-scoped child that closes when the block exits. For cases where you need a scope whose lifetime isn't tied to a block—such as class-level resource management or resources shared across method calls—use open():
import zio.blocks.scope._
// From Scope.global, open() returns OpenScope directly ($[A] = A for global):
val os: Scope.OpenScope = Scope.global.open()
// Use the child scope to allocate resources:
val db = os.scope.allocate(Resource(new Database))
// ... use db across multiple method calls or threads ...
// Explicitly close when done (runs finalizers, returns Finalization):
os.close()Within a child scope, open() returns $[OpenScope], so you need to unwrap it:
Scope.global.scoped { scope =>
import scope._
val os: $[Scope.OpenScope] = scope.open()
val openScope: Scope.OpenScope = leak(os) // unwrap the scoped wrapper
val db = openScope.scope.allocate(Resource(new Database))
// ...
openScope.close()
}OpenScope is a case class with two fields:
scope: Scope— the child scope for allocating resources and registering finalizersclose: () => Finalization— explicitly closes the scope and runs its finalizers
Key properties of open():
- The child scope is unowned:
isOwneralways returnstrue, so it can be used from any thread - The child is still parent-linked: if the parent scope closes before
close()is called, the child's finalizers still run (the parent registered a safety finalizer) - Calling
close()cancels the parent's safety finalizer to avoid double-finalization - You must keep the
OpenScopehandle and callclose()when done to release resources deterministically
For manual wiring (when you already have dependencies assembled), use wire.toResource(ctx):
import zio.blocks.scope._
import zio.blocks.context.Context
final case class Config(debug: Boolean)
// Wire.shared[Config] infers the constructor deps (here: Boolean → Config):
val w: Wire.Shared[Boolean, Config] = Wire.shared[Config]
val deps: Context[Boolean] = Context[Boolean](true)
Scope.global.scoped { scope =>
import scope._
val cfg: $[Config] = allocate(w.toResource(deps))
val debug: $[Boolean] = scope.use(cfg)(_.debug)
println(debug) // $[Boolean] = Boolean at runtime
}Sharing vs uniqueness at the wire level:
import zio.blocks.scope._
val ws = Wire.shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
val wu = Wire.unique[Config] // unique recipe; each allocation is freshResource.from[T](wires*) is the primary entry point for dependency injection. It resolves T and all its dependencies into a single Resource[T].
import zio.blocks.scope._
final case class Config(url: String)
final class Logger {
def info(msg: String): Unit = println(msg)
}
final class Database(cfg: Config) extends AutoCloseable {
def query(sql: String): String = s"[${cfg.url}] $sql"
def close(): Unit = println("database closed")
}
final class Service(db: Database, logger: Logger) extends AutoCloseable {
def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
def close(): Unit = println("service closed")
}
// Only provide leaf values (primitives, configs) - the rest is auto-wired:
val serviceResource: Resource[Service] =
Resource.from[Service](
Wire(Config("jdbc:postgresql://localhost/db"))
)
Scope.global.scoped { scope =>
import scope._
val svc: $[Service] = allocate(serviceResource)
scope.use(svc)(_.run())
}
// Output: running with [jdbc:postgresql://localhost/db] SELECT 1
// Then: service closed, database closed (LIFO order)What you must provide:
- Leaf values: primitives, configs, pre-existing instances via
Wire(value) - Abstract types: traits/abstract classes via
Wire.shared[ConcreteImpl] - Overrides: when you want
uniqueinstead of the defaultshared
What is auto-created:
- Concrete classes with accessible primary constructors (default:
Wire.shared)
When a dependency is a trait or abstract class, provide a wire for a concrete implementation:
import zio.blocks.scope._
trait Logger {
def info(msg: String): Unit
}
final class ConsoleLogger extends Logger {
def info(msg: String): Unit = println(msg)
}
final class App(logger: Logger) {
def run(): Unit = logger.info("Hello!")
}
// Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
val appResource: Resource[App] =
Resource.from[App](
Wire.shared[ConsoleLogger]
)
Scope.global.scoped { scope =>
import scope._
val app: $[App] = allocate(appResource)
scope.use(app)(_.run())
}Single instance for diamond patterns:
trait Service
class LiveService extends Service
class NeedsService(s: Service)
class NeedsLive(l: LiveService)
class App(a: NeedsService, b: NeedsLive)
// One LiveService instance satisfies both Service and LiveService dependencies:
val appResource = Resource.from[App](
Wire.shared[LiveService]
)
// count of LiveService instantiations: 1Sometimes you must hand a raw value to code that cannot work with $[A] types.
import zio.blocks.scope._
Scope.global.scoped { scope =>
import scope._
val db: $[Database] = allocate(Resource(new Database))
val raw: Database = leak(db) // emits a compiler warning
// thirdParty(raw)
}Warning: leaking bypasses compile-time guarantees. The value may be used after its scope closes. Use only when unavoidable.
The scope macros produce beautiful, actionable compile-time error messages with ASCII diagrams and helpful hints:
── Scope Error ──────────────────────────────────────────────────────────────
Cannot derive Wire for MyTrait: not a class.
Hint: Use Wire.Shared / Wire.Unique directly.
────────────────────────────────────────────────────────────────────────────
── Scope Error ──────────────────────────────────────────────────────────────
Cannot auto-create String
This type (primitive, collection, or function) cannot be auto-created.
Required by:
├── Config
└── App
Fix: Provide Wire(value) with the desired value:
Resource.from[...](
Wire(...), // provide a value for String
...
)
────────────────────────────────────────────────────────────────────────────
── Scope Error ──────────────────────────────────────────────────────────────
Cannot auto-create Logger
This type is abstract (trait or abstract class).
Required by:
└── App
Fix: Provide a wire for a concrete implementation:
Resource.from[...](
Wire.shared[ConcreteImpl], // provides Logger
...
)
────────────────────────────────────────────────────────────────────────────
── Scope Error ──────────────────────────────────────────────────────────────
Multiple providers for Service
Conflicting wires:
1. LiveService
2. TestService
Hint: Remove duplicate wires or use distinct wrapper types.
────────────────────────────────────────────────────────────────────────────
── Scope Error ──────────────────────────────────────────────────────────────
Dependency cycle detected
Cycle:
┌───────────┐
│ ▼
A ──► B ──► C
▲ │
└───────────┘
Break the cycle by:
• Introducing an interface/trait
• Using lazy initialization
• Restructuring dependencies
────────────────────────────────────────────────────────────────────────────
── Scope Error ──────────────────────────────────────────────────────────────
Dependency type conflict in MyService
FileInputStream is a subtype of InputStream.
When both types are dependencies, Context cannot reliably distinguish
them. The more specific type may be retrieved when the more general
type is requested.
To fix this, wrap one or both types in a distinct wrapper:
case class WrappedInputStream(value: InputStream)
or
opaque type WrappedInputStream = InputStream
────────────────────────────────────────────────────────────────────────────
── Scope Error ──────────────────────────────────────────────────────────────
Constructor of App has multiple parameters of type String
Context is type-indexed and cannot supply distinct values for the same type.
Fix: Wrap one parameter in an opaque type to distinguish them:
opaque type FirstString = String
or
case class FirstString(value: String)
────────────────────────────────────────────────────────────────────────────
When using leak(value) to escape the scoped type system:
── Scope Warning ────────────────────────────────────────────────────────────
leak(db)
^
|
Warning: db is being leaked from scope MyScope.
This may result in undefined behavior.
Hint:
If you know this data type is not resourceful, then add an Unscoped
instance for it so you do not need to leak it.
────────────────────────────────────────────────────────────────────────────
Core methods (Scala 3 using vs Scala 2 implicit differs, but the shapes are the same):
sealed abstract class Scope extends Finalizer {
type $[+A] // = A at runtime (zero-cost)
type Parent <: Scope
val parent: Parent
def allocate[A](resource: Resource[A]): $[A]
def allocate[A <: AutoCloseable](value: => A): $[A]
def defer(f: => Unit): DeferHandle
// Create a non-lexical child scope (usable from any thread)
def open(): $[Scope.OpenScope]
// Apply function to scoped value, returns scoped result
def use[A, B](scoped: $[A])(f: A => B): $[B]
// Construct a scoped value from a raw value
def $[A](a: A): $[A]
// Lower parent-scoped value into this scope
def lower[A](value: parent.$[A]): $[A]
// Escape hatch: unwrap scoped value (emits compiler warning)
// Scala 3:
inline def leak[A](inline sa: $[A]): A // macro — emits warning
// Scala 2:
def leak[A](sa: $[A]): A // macro — emits warning
// Creates a child scope - requires Unscoped evidence on return type
// Scala 3:
def scoped[A](f: (child: Scope.Child[self.type]) => child.$[A])(using Unscoped[A]): A
// Scala 2 (macro rewrites the types; declared signature is untyped):
def scoped(f: Scope.Child[self.type] => Any): Any // macro
implicit class ScopedOps[A](sa: $[A]) {
def map[B](f: A => B): $[B]
def flatMap[B](f: A => $[B]): $[B]
}
implicit def wrapUnscoped[A: Unscoped](a: A): $[A]
}sealed trait Resource[+A]
object Resource {
def apply[A](value: => A): Resource[A]
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
// Macro - DI entry point:
def from[T]: Resource[T] // zero-dep classes
def from[T](wires: Wire[?, ?]*): Resource[T] // with dependency wires
// Low-level constructors (also used by generated code):
def shared[A](f: Scope => A): Resource[A]
def unique[A](f: Scope => A): Resource[A]
}sealed trait Wire[-In, +Out] {
def isShared: Boolean
def shared: Wire.Shared[In, Out]
def unique: Wire.Unique[In, Out]
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
}
object Wire {
// Macro entry points:
def shared[T]: Wire.Shared[?, T] // derive from T's constructor
def unique[T]: Wire.Unique[?, T] // derive from T's constructor
// Wrap pre-existing value (auto-finalizes if AutoCloseable):
def apply[T](value: T): Wire.Shared[Any, T]
final case class Shared[-In, +Out] extends Wire[In, Out]
final case class Unique[-In, +Out] extends Wire[In, Out]
}abstract class DeferHandle {
def cancel(): Unit // Remove the finalizer so it won't run at scope close
}case class OpenScope private[scope] (
scope: Scope, // the child scope (unowned, usable from any thread)
close: () => Finalization // explicitly close the scope and run its finalizers
)- Use
Scope.global.scoped { scope => import scope._; ... }to create a safe region. - For simple resources:
allocate(Resource(value))orallocate(Resource.acquireRelease(...)(...)) - For dependency injection:
allocate(Resource.from[App](Wire(config), ...))— auto-wires concrete classes, you provide leaves and overrides. - Use
scope.use(value)(f)to work with scoped values — all operations are eager. $[A] = Aat runtime — zero-cost opaque type.- The
scopedmethod requiresUnscoped[A]evidence on the return type. - Use
lower(parentValue)to access parent-scoped values in child scopes. - Use
scope.open()for non-lexical child scopes (class-level, cross-thread); keep theOpenScopehandle and callclose()when done. - Classes can accept
Scope(full capabilities) orFinalizer(onlydefer) as DI-injected parameters. deferreturns aDeferHandlefor O(1) cancellation of registered finalizers.- Return
Unscopedtypes from child scopes to extract raw values. - If it doesn't typecheck, it would have been unsafe at runtime.
The 3 DI macro entry points:
Wire.shared[T]— shared wire from constructorWire.unique[T]— unique wire from constructorResource.from[T](wires*)— wire up T and all dependencies