ZIO Prelude Documentation

repository·series/2.x·Indexed 19 days ago

https://github.com/zio/zio-prelude

A lightweight library of functional abstractions and data types for Scala developers. ZIO Prelude provides high-performance alternatives to traditional functional programming libraries, focusing on modularity and ZIO integration. It includes type classes for Associative, Commutative, Idempotent, Identity, and Hash operations, as well as utilities like Subtype for increased type safety and NonEmptyMap for guaranteed non-empty collections.

Tokens
49.5K
Snippets
150
Records
184
Agent score
65%

What's inside ZIO Prelude

  1. Overview of ZIO Prelude capabilities

    series/2.x

    ZIO Prelude is a lightweight library providing functional abstractions and data types with tight ZIO integration. It focuses on three main areas:

    1. Data structures and traversal: Extends Scala standard library collections with new instances and useful additions.
    2. Patterns of composition for types: Provides binary operators for combining two values of the same type, named after algebraic laws like Associative, Commutative, and Identity.
    3. Patterns of composition for type constructors: Provides binary operators for type constructors (e.g., Future, Option, ZIO) that produce structures like tuples or Eithers.

    Key features include:

    • Functional Data Types: Types like NonEmptyList, NonEmptySet, ZSet, ZNonEmptySet, Validation, and ZValidation for accurate domain modeling.
    • Functional Abstractions: Ways to combine data in a principled manner.
    • New Types: Zero-overhead Subtype wrappers to increase type safety in domain modeling.
    • ZPure: A high-performance alternative to monad transformers that supports logging, context, state, and errors.
    • zio.prelude.fx: A research-stage package providing abstractions over expressive effect types like ZIO and ZPure.
  2. Explore Functional Data Types in ZIO Prelude

    series/2.x

    ZIO Prelude provides several specialized data types designed to model domains more accurately and solve common functional programming problems. These types include:

    • Equivalence: Defines an equivalence relationship between two data types.
    • NonEmptyList: A list guaranteed to contain at least one element.
    • These: A type representing a choice between Left(A), Right(B), or Both(A, B), ideal for merging data streams.
    • Validation: A type that represents either a success or an accumulation of one or more errors.
    • ZSet: A generalized set supporting measures of element frequency (e.g., multi-sets or fuzzy sets).
    • ZValidation: A generalization of Validation that accumulates both errors and a log of warnings.
  3. What is Ord and how does it differ from Scala's Ordering?

    series/2.x

    Ord[A] describes a total ordering on values of type A. It allows you to compare any two values and determine if the left value is LessThan, GreaterThan, or Equals the right value.

    Key Differences from scala.math.Ordering

    1. Variance and Type Inference: Unlike the invariant scala.math.Ordering, Ord is contravariant (Ord[-A]). This means if you have an Ord[CustomerAccount], you can automatically use it to compare ConsumerAccount or BusinessAccount subtypes without manual type widening. This solves common type inference failures in Scala when comparing subtypes.
    2. Result Type: Instead of returning an Int (where negative/positive/zero is used), Ord returns a sealed trait Ordering (LessThan, GreaterThan, or Equals), which is more expressive and type-safe.
    3. Integration: Ord integrates with other ZIO Prelude abstractions and provides operators like =?= and <> for easier composition.

    Interoperability

    import zio.prelude.Ord
    
    // Convert Scala Ordering to Ord
    val toScala: scala.math.Ordering[Int] =
      Ord[Int].toScala
    
    // Convert Ord to Scala Ordering
    val fromScala: Ord[Int] =
      Ord.fromScala(scala.math.Ordering[Int])
  4. What is NonEmptyForEach and when to use it

    series/2.x

    The NonEmptyForEach[F] abstraction describes a parameterized type F[A] that is guaranteed to contain one or more values of type A.

    It is a specialized version of the ForEach abstraction. While ForEach is used for collections that might be empty (like Chunk or List), NonEmptyForEach is used for collections that are guaranteed to have at least one element (like NonEmptyChunk, NonEmptyList, or certain tree structures).

    Using NonEmptyForEach allows you to perform reductions and transformations that do not require an 'identity' or 'neutral' element, because you can always start the operation with the first element of the collection.

  5. What is NonEmptyList and when to use it

    series/2.x

    NonEmptyList is a data type representing a List that is guaranteed to contain at least one element. It is defined by two cases: Single[A](head: A) and Cons[A](head: A, tail: NonEmptyList[A]).

    Why use it?

    • Accurate Domain Modeling: Use it when your business logic dictates a collection cannot be empty (e.g., a collection of errors that only exists if an error occurred).
    • Type-Level Safety: Unlike Scala's standard List, NonEmptyList allows you to call methods like head, reduceLeft, or reduceRight without fear of exceptions or having to handle empty cases.
    • Preserving Information: Many operations on NonEmptyList (like map) return a NonEmptyList, preserving the knowledge that the collection is not empty, whereas standard library operations often revert to a standard List to account for potential emptiness.
    sealed trait NonEmptyList[+A]
    object NonEmptyList {
      case class Single[A](head: A)                      extends NonEmptyList[A]
      case class Cons[A](head: A, tail: NonEmptyList[A]) extends NonEmptyList[A]
    }
  6. What is the Inverse abstraction?

    series/2.x

    The Inverse[A] abstraction describes a type that supports a combine operator (to add structure) and an inverse operator (to remove structure). It extends Identity[A], which provides an identity value.

    Crucially, inverse is a binary operator, not a unary one. This allows for defining subtraction-like behavior even for types that do not have negative representations (like Natural numbers or Set).

    Key properties:

    • combine(left, right) adds structure.
    • inverse(left, right) undoes the structure added by combine.
    • Applying inverse to a value and itself returns the identity: inverse(a, a) === identity.

    Core traits:

    trait Associative[A] {
      def combine(left: => A, right: => A): A
    }
    
    trait Identity[A] extends Associative[A] {
      def identity: A
    }
    
    trait Inverse[A] extends Identity[A] {
      def inverse(left: => A, right: => A): A
    }
    trait Associative[A] {
      def combine(left: => A, right: => A): A
    }
    
    trait Identity[A] extends Associative[A] {
      def identity: A
    }
    
    trait Inverse[A] extends Identity[A] {
      def inverse(left: => A, right: => A): A
    }
  7. What is Contravariant and how to use contramap

    series/2.x

    A Contravariant type F[A] is a parameterized type that potentially consumes A values but never produces them.

    To transform an F[A] into an F[B], you use the contramap operator with a function of type B => A. This is the opposite of the map operator, which uses A => B to transform outputs. contramap allows you to "work backwards" by adapting the inputs required by the data type.

    Common examples of contravariant types include:

    • Functions (with respect to their inputs)
    • ZIO (with respect to its environment type)
    • ZSink (with respect to its input type)

    Laws:

    • fa.contramap(identity) === fa
    • fa.contramap(f).contramap(g) === fa.contramap(f.compose(g))
    // If you import zio.prelude._, you can use contramap to transform F[A] to F[B]
    // using a function B => A
    val fa: F[A] = ???
    val f: B => A = ???
    val fb: F[B] = fa.contramap(f)
  8. What is an Invariant and how to use it

    series/2.x

    An Invariant[F] describes a parameterized type F[A] that both consumes and produces A values. It provides the invmap operator, which allows you to "lift" an Equivalence[A, B] (an A <=> B relationship) into an equivalence relationship between F[A] and F[B].

    Core Concepts

    • Equivalence (<=>): A data type representing a bidirectional transformation between two types A and B without losing information. It consists of a to: A => B function and a from: B => A function.
    • Lifting with invmap: If you have a way to convert A to B and back, invmap provides a way to convert a container/context of As into a container/context of Bs.

    Laws

    To be a valid Invariant, the implementation must follow these laws:

    1. Identity: fa.invmap(Equivalence.identity) === fa
    2. Composition: fa.invmap(f).invmap(g) === fa.invmap(f.andThen(g))

    Common Invariant Types

    • JsonCodec[A] (where A is both encoded and decoded)
    • Scala's Set[A]
    • ZIO's Ref[A], Queue[A], and Hub[A]
    // Using invmap in infix form (requires importing zio.prelude._)
    val codecB: JsonCodec[B] = codecA.invmap(equivalenceAB)
  9. What is ZPure and when should I use it?

    series/2.x

    A ZPure[W, S1, S2, R, E, A] is a description of a pure computation that provides four specific capabilities in a single, high-performance data type:

    • Errors (E): Can fail with an error of type E (similar to Either).
    • Context (R): Requires an environment of type R (similar to Reader).
    • State (S1, S2): Can transition from an initial state S1 to an updated state S2 (similar to State).
    • Logging (W): Produces a log of type W (similar to Writer).

    When to use ZPure vs ZIO: Use ZPure when you want to model computations that use these four capabilities without embedding arbitrary side-effecting code. If your computation requires managing IO or concurrency, use ZIO instead. ZPure is designed for high performance and better ergonomics than traditional monad transformer stacks.

    // The type signature represents:
    // W: Log type
    // S1: Initial state type
    // S2: Updated state type
    // R: Environment type
    // E: Error type
    // A: Success value type
    case class ZPure[+W, -S1, +S2, -R, +E, +A](run: (R, S1) => (Chunk[W], Either[E, (S2, A)]))
  10. What is AssociativeFlatten and how does it work?

    series/2.x

    The AssociativeFlatten[F] abstraction describes a way to combine two layers of a nested value F[F[A]] into a single layer F[A] in an associative manner.

    Conceptually, flatten represents running an outer value to produce an inner value, and then running that inner value to produce a final result. This allows for dynamic workflows where the next step depends on the result of the previous step.

    Associative Law

    The flatten operator must satisfy the following law:

    fffa.flatten.flatten == fffa.map(_.flatten).flatten

    Common Interpretations

    Different data types interpret flatten differently based on their semantics:

    • ZIO: Runs the first workflow, and if successful, runs the resulting workflow.
    • ZStream: Concatenates the inner streams into a single stream.
    • Either / Option: If the outer layer is a success, returns the inner value; otherwise, returns the failure/none.
    • Collections (e.g., Chunk): Concatenates the inner collections into a single collection.
    trait AssociativeFlatten[F[+_]] {
      def flatten[A](ffa: F[F[A]]): F[A]
    }
  11. What is the Commutative abstraction?

    series/2.x

    The Commutative[A] abstraction describes a data type that has a combine operator which is both associative and commutative.

    While Associative[A] only guarantees that the order of operations does not matter (e.g., (a + b) + c == a + (b + c)), Commutative[A] further guarantees that the order of the values themselves does not matter (e.g., a + b == b + a).

    Key Differences

    • Associative but NOT Commutative: String concatenation ("a" + "b" != "b" + "a").
    • Associative AND Commutative: Integer addition (2 + 3 == 3 + 2).

    Using Commutative at the type level allows you to write code that is safe to use in environments where the order of operations is non-deterministic, such as concurrent or distributed systems.

    trait Associative[A] {
      def combine(left: => A, right: => A): A
    }
    
    trait Commutative[A] extends Associative[A]
  12. What is a ZSet and how does it work?

    series/2.x

    A ZSet[A, B] is a generalized version of a set where the measure B represents how many times an element A appears. Conceptually, it behaves like a Map[A, B].

    Common variants include:

    • Set: ZSet[A, Boolean], where an element is either present (true) or absent (false).
    • MultiSet (Bag): ZSet[A, Natural], where the measure is a non-negative integer representing counts (e.g., for a shopping cart).
    • Probabilistic Set: ZSet[A, Double], where the measure represents a probability.
    • Signed Set: ZSet[A, Int], where elements can have negative counts.
    import zio.prelude.ZSet
    import zio.prelude.newtypes.Natural
    
    type Set[+A] = ZSet[A, Boolean]
    type MultiSet[+A] = ZSet[A, Natural]