|
| 1 | +# Cats Effect Resource (Scala) - Practical Guide |
| 2 | + |
| 3 | +Sources: |
| 4 | +- https://typelevel.org/cats-effect/docs/std/resource |
| 5 | +- https://github.com/typelevel/cats-effect/blob/series/3.x/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala |
| 6 | + |
| 7 | +## Table of Contents |
| 8 | +- [Core model](#core-model) |
| 9 | +- [Core APIs](#core-apis) |
| 10 | +- [When to use Resource](#when-to-use-resource) |
| 11 | +- [Patterns](#patterns) |
| 12 | +- [Cancelation and error behavior](#cancelation-and-error-behavior) |
| 13 | +- [Interop and blocking](#interop-and-blocking) |
| 14 | +- [Checklist](#checklist) |
| 15 | + |
| 16 | +## Core model |
| 17 | +- `Resource[F, A]` encodes acquisition and release with a `use` phase. |
| 18 | +- Release runs on success, error, or cancelation. |
| 19 | +- Acquisition and finalizers are sequenced and run in a controlled scope; release is LIFO. |
| 20 | + |
| 21 | +## Core APIs |
| 22 | +- `Resource.make(acquire)(release)` for custom lifecycle. |
| 23 | +- `Resource.fromAutoCloseable` for `AutoCloseable` lifecycles. |
| 24 | +- `Resource.eval` to lift an effect into a resource. |
| 25 | +- `.use` to run the resource and ensure release. |
| 26 | +- `map`, `flatMap`, `mapN`, `parMapN`, `parZip` to compose resources. |
| 27 | + |
| 28 | +## When to use Resource |
| 29 | +- You need safe cleanup under cancelation. |
| 30 | +- You need to compose resources and guarantee LIFO release. |
| 31 | +- You want an API that makes lifecycle explicit and testable. |
| 32 | + |
| 33 | +## Patterns |
| 34 | + |
| 35 | +### 1) Resource constructors |
| 36 | +Prefer functions that return `Resource[F, A]`: |
| 37 | + |
| 38 | +```scala |
| 39 | +import cats.effect.{Resource, Sync} |
| 40 | + |
| 41 | +final class UserProcessor { |
| 42 | + def start(): Unit = () |
| 43 | + def shutdown(): Unit = () |
| 44 | +} |
| 45 | + |
| 46 | +def userProcessor[F[_]: Sync]: Resource[F, UserProcessor] = |
| 47 | + Resource.make(Sync[F].delay { new UserProcessor().tap(_.start()) })(p => |
| 48 | + Sync[F].delay(p.shutdown()) |
| 49 | + ) |
| 50 | +``` |
| 51 | + |
| 52 | +### 2) Composing resources |
| 53 | + |
| 54 | +```scala |
| 55 | +import cats.effect.{Resource, Sync} |
| 56 | +import cats.syntax.all._ |
| 57 | + |
| 58 | +final class DataSource { def connect(): Unit = (); def close(): Unit = () } |
| 59 | +final class Service(ds: DataSource, up: UserProcessor) |
| 60 | + |
| 61 | +def dataSource[F[_]: Sync]: Resource[F, DataSource] = |
| 62 | + Resource.make(Sync[F].delay { new DataSource().tap(_.connect()) })(ds => |
| 63 | + Sync[F].delay(ds.close()) |
| 64 | + ) |
| 65 | + |
| 66 | +def service[F[_]: Sync]: Resource[F, Service] = |
| 67 | + (dataSource[F], userProcessor[F]).mapN(new Service(_, _)) |
| 68 | +``` |
| 69 | + |
| 70 | +### 3) Parallel acquisition |
| 71 | + |
| 72 | +```scala |
| 73 | +import cats.effect.{Resource, Sync} |
| 74 | +import cats.syntax.all._ |
| 75 | + |
| 76 | +def servicePar[F[_]: Sync]: Resource[F, Service] = |
| 77 | + (dataSource[F], userProcessor[F]).parMapN(new Service(_, _)) |
| 78 | +``` |
| 79 | + |
| 80 | +### 4) File input stream |
| 81 | + |
| 82 | +```scala |
| 83 | +import cats.effect.{IO, Resource} |
| 84 | + |
| 85 | +import java.io.FileInputStream |
| 86 | + |
| 87 | +def inputStream(path: String): Resource[IO, FileInputStream] = |
| 88 | + Resource.fromAutoCloseable(IO.blocking(new FileInputStream(path))) |
| 89 | +``` |
| 90 | + |
| 91 | +### 5) Database pool + per-connection resource |
| 92 | + |
| 93 | +```scala |
| 94 | +import cats.effect.{Resource, Sync} |
| 95 | + |
| 96 | +import javax.sql.DataSource |
| 97 | + |
| 98 | +def pool[F[_]: Sync]: Resource[F, DataSource] = ??? |
| 99 | + |
| 100 | +def connection[F[_]: Sync](ds: DataSource): Resource[F, java.sql.Connection] = |
| 101 | + Resource.make(Sync[F].blocking(ds.getConnection))(c => |
| 102 | + Sync[F].blocking(c.close()) |
| 103 | + ) |
| 104 | +``` |
| 105 | + |
| 106 | +### 6) Acquire in a loop |
| 107 | +Use `Resource.make` per element and compose with `traverse`/`parTraverse`: |
| 108 | + |
| 109 | +```scala |
| 110 | +import cats.effect.{Resource, Sync} |
| 111 | +import cats.syntax.all._ |
| 112 | + |
| 113 | +def acquireOne[F[_]: Sync](id: String): Resource[F, Handle] = ??? |
| 114 | + |
| 115 | +def acquireAll[F[_]: Sync](ids: List[String]): Resource[F, List[Handle]] = |
| 116 | + ids.traverse(acquireOne[F]) |
| 117 | +``` |
| 118 | + |
| 119 | +## Cancelation and error behavior |
| 120 | +- Finalizers run on success, error, or cancelation. |
| 121 | +- If finalizers can fail, decide whether to log, suppress, or raise secondary errors. |
| 122 | +- Keep finalizers idempotent and minimal to avoid cascading failures during release. |
| 123 | + |
| 124 | +## Interop and blocking |
| 125 | +- Wrap blocking acquisition or release in `blocking` to avoid compute starvation. |
| 126 | +- Prefer `Resource.fromAutoCloseable` for Java interop; use `make` for custom release. |
| 127 | +- If the API supports cooperative cancellation, combine it with `Resource` to ensure cleanup. |
| 128 | + |
| 129 | +## Checklist |
| 130 | +- Expose `Resource[F, A]` in public constructors. |
| 131 | +- Keep release idempotent and tolerant of partial failures. |
| 132 | +- Use `parMapN` only for independent resources. |
| 133 | +- Avoid calling `.use` except at lifecycle boundaries. |
| 134 | +- Use `IO.blocking`/`Sync[F].blocking` for blocking JVM APIs. |
0 commit comments