Cats Effect Documentation

repository·series/3.x·Indexed 22 days ago

https://github.com/typelevel/cats-effect

A high-performance, asynchronous, and composable framework for building purely functional applications in Scala. It provides the IO monad for effect control and a set of typeclasses powering a functional library ecosystem. Version 3.7.0 supports Scala 2.12, 2.13, 3.2, and Scala.js 1.13. The framework includes specialized modules such as cats-effect-kernel, cats-effect-laws, cats-effect-std, and cats-effect-testkit, as well as comprehensive runtime monitoring via PollerMetrics, WorkStealingThreadPoolMetrics, and JMX MBeans.

Tokens
59.8K
Snippets
148
Records
246
Agent score
77%

What's inside Cats Effect

  1. What is a Fiber and the Spawn typeclass?

    series/3.x

    The Spawn typeclass provides a lightweight, semantic thread-like abstraction called a Fiber. Unlike JVM Threads, which are expensive and limited in number, Fibers are extremely lightweight (roughly 128 bytes in IO) and can be scaled to millions of concurrent instances.

    Fibers represent parallel semantic threads of execution. The runtime handles the mapping of these fibers to actual kernel threads in an optimal way for the specific platform (e.g., JVM or JavaScript).

    While Fibers are often used as implementation details for higher-level concurrency tools, you interact with them primarily through the .start method provided by Spawn.

  2. What is an effect and how does it differ from a side-effect?

    series/3.x

    In Cats Effect, an effect is a description of an action (or actions) that will be taken when evaluation happens. An effect is a value that can be passed around, composed, and reused without immediately performing any work.

    A side-effect is an action that causes changes outside of just returning a value (e.g., printing to console, changing a var, making a network call). Unlike effects, side-effects happen immediately when the code is executed and are harder to control (e.g., you cannot easily schedule, retry, or parallelize a raw side-effect).

    Key distinction:

    • IO[A] is a description of an action (an effect).
    • The execution of that IO is what performs the side-effect.

    This allows you to treat logic as data, enabling safe concurrency and better resource management.

  3. What is `AtomicMap` and when to use it

    series/3.x

    An AtomicMap[F, K, V] is a total map that associates keys of type K with AtomicCell[F, V] values.

    Mental Model: Think of it as a MapRef (a reference to a Map) that allows for effectual updates on a per-key basis. It is conceptually similar to an AtomicCell[F, Map[K, V]], but provides better ergonomics for key-specific operations and significantly less contention.

    Key Characteristics:

    • Per-key Granularity: Operations are performed on individual key-value pairs. This allows concurrent updates to different keys to proceed independently (sharding by key).
    • No Multi-key Atomicity: While individual keys are managed atomically, AtomicMap does not support atomic updates involving multiple keys simultaneously.
    • Use Case: Use it when you need to perform effectual updates (e.g., involving IO) on specific values within a map without locking the entire map for every operation.
  4. What is a Mutex and how to use it

    series/3.x

    A Mutex is a concurrency primitive used to ensure that only one fiber has access to a resource at a time. It functions as a Semaphore with exactly one available permit.

    Warning: Non-reentrancy Mutex is not reentrant. Attempting to acquire the lock while already holding it (e.g., calling mutex.lock.surround(mutex.lock.use_)) will result in a deadlock.

    import cats.effect.Resource
    
    trait Mutex[F[_]] {
      def lock: Resource[F, Unit]
    }
  5. What is a Dispatcher and when to use one

    series/3.x

    A Dispatcher is a fiber-based Supervisor utility used to evaluate effects across an impure boundary.

    It is specifically designed for integrating with reactive or impure interfaces that produce values via callbacks (e.g., a method returning Unit instead of an effectful type like IO). In these scenarios, you cannot simply return an effect from the callback because the callback expects a side effect, not a description of an effect. Dispatcher allows you to bridge this gap by executing the effect asynchronously from within the impure callback.

    Key characteristics:

    • It can be derived for any effect type conforming to the Async typeclass.
    • Creating an instance is very cheap; you are encouraged to instantiate it where necessary rather than wiring a single instance throughout an application.
  6. What is IOLocal and how does it behave?

    series/3.x

    IOLocal provides a way to manipulate a context across different scopes. It can be viewed as an alternative to cats.data.Kleisli for managing context, but it should not be treated as a Ref because it follows different laws.

    Key behavioral characteristics:

    • Fiber Isolation: When a fiber is forked (e.g., using Spawn[F].start), the new fiber operates on a copy of the parent's IOLocal context. Modifications made in the child fiber are not reflected in the parent.
    • One-way Visibility: While children inherit a copy of the parent's state at the moment of forking, parent operations on the IOLocal are invisible to children that have already been forked.
    • Sibling Isolation: Two fibers forked from the same parent will each see their own independent modifications; they will never see each other's changes.
  7. What is Backpressure and how does it work?

    series/3.x

    Backpressure in Cats Effect allows you to run effects through a rate-limiting strategy. It is used in scenarios where a large number of tasks need to be processed, but system resources or time are limited.

    There are two available strategies:

    • Lossy: An effect will not be run if backpressure is present.
    • Lossless: An effect will run, but it will semantically block until the backpressure is alleviated.

    You can instantiate Backpressure for your effect type (e.g., IO) by providing a strategy and a capacity limit.

    trait Backpressure[F[_]] {
      def metered[A](f: F[A]): F[Option[A]]
    }
  8. What is MapRef and when to use it

    series/3.x

    A MapRef[F, K, V] is a total map that associates a key K with a Ref[F, V] of its value.

    It is conceptually similar to a Ref[F, Map[K, V]], but provides better ergonomics when you need to work on a per-key basis.

    Key Characteristics:

    • Per-key access: You can access the specific Ref for a key using apply(k).
    • No multi-key atomicity: It does not support atomic updates to multiple keys simultaneously.
    • Reduced contention: Implementations like MapRef.ofShardedImmutableMap allow concurrent updates to different keys to execute independently if the keys belong to different shards.
    • Total map: Because it is a total map, constructors either require a default value or return a MapRef[F, K, Option[V]].
    import cats.effect.Ref
    
    trait MapRef[F[_], K, V] {
      /**
       * Access the reference for this Key
       */
      def apply(k: K): Ref[F, V]
    }
  9. Understand why Outcome#Succeeded contains F[A]

    series/3.x

    In Cats Effect 3, Outcome#Succeeded contains a value of type F[A] instead of A. This design choice was made to support monad transformers. For example, if you are using OptionT[IO, A], a successful fiber join will return an Outcome where the Succeeded case wraps the OptionT itself, rather than the underlying A.

    When working with IO, you can assume that binding on the F[A] value inside Succeeded does not perform additional effects, as it is typically constructed as Outcome.Succeeded(IO.pure(result)).

    val oc: OutcomeIO[Int] =
      for {
        fiber <- Spawn[OptionT[IO, *]].start(OptionT.none[IO, Int])
        oc <- fiber.join
      } yield oc
  10. Use structured concurrency for safe concurrent execution

    series/3.x

    Structured concurrency ensures that concurrent operations form a closed hierarchy. This means any operation that forks actions must ensure those actions complete before the parent moves forward, and results are only available upon completion.

    Recommended Tools:

    • parTupled: Evaluates a pair of independent effects and produces a tuple of their results.
    • parMapN: Parallel version of mapping over a collection.
    • parTraverse: Parallel version of traversing a collection.
    • background: For running tasks in the background within a controlled scope.
    • Supervisor: For managing groups of fibers.
    • Dispatcher: For managing resource-related concurrency.

    Avoid Unstructured Concurrency unless necessary: Using start without waiting for completion can lead to 'fiber leaks' where references to a running fiber are lost. For shared state, while Ref and Deferred are powerful, they are fundamentally unstructured and can make business logic harder to follow. Use them to build higher-level abstractions like Queue or Semaphore instead of using them directly in complex logic.

  11. Understand why Env uses F[Option[String]] for lookups

    series/3.x

    The Env[F] service returns F[Option[String]] instead of a pure Option[String] for two primary reasons:

    1. Platform-specific side effects: On some platforms (like POSIX), environment variables can be modified within the current process. While Java's public API doesn't support this, other runtimes like Scala Native or via JNA might.
    2. Error handling: Retrieving an environment variable can fail due to platform-specific issues, such as a SecurityException on Windows if permissions are insufficient. Wrapping the result in F allows the library to account for these platform peculiarities across JVM, Scala Native, and ScalaJS.
  12. Transform Queues using QueueSource and QueueSink

    series/3.x

    A Queue[F, A] can be treated as a QueueSource or a QueueSink to allow for type transformations using Functor and Contravariant instances.

    • QueueSource[F, A]: Use Functor[QueueSource[F, *]].map(q)(f) to transform a queue of type A into a queue of type B (where f: A => B). This is useful for reading transformed values.
    • QueueSink[F, A]: Use Contravariant[QueueSink[F, *]].contramap(q)(f) to transform a queue of type B into a queue of type A (where f: B => A). This is useful for writing values that need transformation before being queued.
    import cats.{Contravariant, Functor}
    import cats.implicits._
    import cats.effect._
    import cats.effect.std.{Queue, QueueSource, QueueSink}
    import cats.effect.unsafe.implicits.global
    
    def covariant(list: List[Int]): IO[List[Long]] = (
      for {
        q <- Queue.bounded[IO, Int](10)
        qOfLongs: QueueSource[IO, Long] = Functor[QueueSource[IO, *]].map(q)(_.toLong)
        _ <- list.traverse(q.offer(_))
        l <- List.fill(list.length)(()).traverse(_ => qOfLongs.take)
      } yield l
    )
    
    def contravariant(list: List[Boolean]): IO[List[Int]] = (
      for {
        q <- Queue.bounded[IO, Int](10)
        qOfBools: QueueSink[IO, Boolean] =
          Contravariant[QueueSink[IO, *]].contramap(q)(b => if (b) 1 else 0)
        _ <- list.traverse(qOfBools.offer(_))
        l <- List.fill(list.length)(()).traverse(_ => q.take)
      } yield l
    )