Monocle Documentation

repository·master·Indexed 23 days ago

https://github.com/optics-dev/monocle

A Scala library for optics providing tools for functional data access and manipulation. It includes a hierarchy of optics such as Fold, Getter, POptional, PLens, PPrism, and Iso, as well as specialized utilities like Plated for recursive structures and Index for accessing values within structures like List, Vector, Map, and String.

Tokens
15.6K
Snippets
34
Records
84
Agent score
82%

What's inside Monocle

  1. Overview of Monocle modules

    master

    Monocle is split into several modules to allow for selective dependency inclusion:

    • core: Contains the primary optics (e.g., Lens, Prism, Traversal) and type class definitions (e.g., Index, Each, Plated), along with instances for standard library and cats data types.
    • macro: Provides macros to simplify the generation of optics.
    • laws: Contains laws for the optics and type classes.
    • refined: Provides optics and type class instances for use with refined refinement types.
    • generic (deprecated): Optics and type class instances for HList and Coproduct from shapeless.
    • state (deprecated): Provides conversion between optics and State or Reader.
    • unsafe (deprecated): Provides optics that do not fully satisfy laws but offer high convenience.
    • tests: Contains tests to verify that optics and type class instances satisfy their laws.
    • bench: Contains JMH benchmarks for measuring optics performance.
    • docs: Source for the Monocle documentation website.
  2. What is a Traversal

    master

    A Traversal is a generalization of an Optional that allows you to focus from a source type S into zero to $n$ values of a target type A. It is commonly used to focus on all elements within a container (like List, Vector, or Option).

    Because a Traversal is also a Fold, you can use it to query data using methods like getAll, headOption, find, and all.

  3. What is an Iso and how to use it

    master

    An Iso is an optic that provides a lossless, symmetric transformation between two types S and A. It is defined by two total functions:

    1. get: S => A
    2. reverseGet (also accessible via apply): A => S

    Because the transformation is symmetric, you can use .reverse to flip the direction of the Iso.

    Common use cases include:

    • Converting between a case class and its tuple representation.
    • Converting between different collection types (e.g., List[A] and Vector[A]).
    • Lifting functions from one type to another (e.g., using a String as a List[Char]).
    import monocle.Iso
    
    // Manual creation between Person and (String, Int)
    val personToTuple = Iso[Person, (String, Int)](p => (p.name, p.age)){case (name, age) => Person(name, age)}
    
    personToTuple.get(Person("Zoe", 25))
    personToTuple(("Zoe", 25)) // Using apply as reverseGet
    
    // Reversing an Iso
    def listToVector[A] = Iso[List[A], Vector[A]](_.toVector)(_.toList)
    def vectorToList[A] = listToVector[A].reverse
    
    // Lifting functions
    val stringToList = Iso[String, List[Char]](_.toList)(_.mkString(""))
    stringToList.modify(_.tail)("Hello")
  4. What is a Prism and how to use it

    master

    A Prism[S, A] is an optic used to select a part of a Sum type (also known as a Coproduct), such as a sealed trait or Enum. It allows you to focus on a specific subtype and provides a way to either extract the value if the type matches or reconstruct the original type from the value.

    Key capabilities:

    • Extraction: Use getOption(s: S): Option[A] to attempt to extract the part A from the sum S.
    • Reconstruction: Use reverseGet(a: A): S (or the shorthand apply(a: A)) to reconstruct the sum type from the part.
    • Pattern Matching: Prisms can be used directly in Scala pattern matching.
    • Transformation: Use replace and modify to update the value if the type matches, or replaceOption and modifyOption to handle success/failure explicitly.
    • Composition: Prisms compose with other optics using andThen.
  5. What is Focus and how to use it

    master

    Focus is the primary entry point for Monocle. It allows you to define a path into an immutable object structure. Once a path is defined, you can perform operations like getting, replacing, or modifying the value at the end of that path.

    To use Focus, you typically import monocle.syntax.all._ to enable the .focus() extension method on your objects.

  6. What is an Optional optic?

    master

    An Optional[S, A] is an optic used to zoom into a Product (such as a case class, Tuple, HList, or Map) where the target element A may or may not exist within the source S.

    Unlike a Lens, which assumes the target is always present, an Optional handles the possibility of absence using Option semantics. To create an Optional, you must provide two functions:

    1. getOption: S => Option[A]: How to retrieve the target element if it exists.
    2. replace: A => S => S: How to replace the target element within the source structure.

    Common use cases include accessing the head of a List or a specific key in a Map that might be missing.

    import monocle.Optional
    
    // Example: An Optional that focuses on the head of a List[Int]
    val head = Optional[List[Int], Int] {
      case Nil => None
      case x :: xs => Some(x)
    }{
       a => {
         case Nil => Nil
         case x :: xs => a :: xs
       }
    }
  7. What is a Lens and how to use it

    master

    A Lens[S, A] is an optic used to zoom into a Product (like a case class, Tuple, HList, or Map) to focus on an element of type A within a structure of type S.

    To manually create a Lens, you provide a get function and a replace function:

    val streetNumber = Lens[Address, Int](_.streetNumber)(n => a => a.copy(streetNumber = n))

    Once created, you can use the following methods:

    • get(s: S): A: Retrieves the focused value.
    • replace(a: A)(s: S): S: Replaces the focused value with a new one.
    • modify(f: A => A)(s: S): S: Updates the focused value using a function (equivalent to get followed by replace).
    • modifyF(f: A => F[A])(s: S): F[S]: Updates the focused value within a context F (requires a Functor instance for F). This is useful for asynchronous updates (e.g., using Future).
    • andThen(other: Lens[A, B]): Lens[S, B]: Composes the current lens with another to zoom deeper into the structure.
    val address = Address(10, "High Street")
    
    streetNumber.get(address)
    streetNumber.replace(5)(address)
    streetNumber.modify(_ + 1)(address)
  8. Verify Lens correctness with LensLaws

    master

    A valid Lens must satisfy specific algebraic laws. You can verify your custom lenses using LensTests from the law module.

    Key laws include:

    • getReplace: If you get a value and then replace it back, the object should be identical to the original (l.replace(l.get(s))(s) == s). This ensures replace doesn't have unintended side effects on other fields.
    • replaceGet: If you replace a value, you must be able to get that exact same value back (l.get(l.replace(a)(s)) == a). This ensures the lens is actually targeting the correct field.
  9. How optics compose with each other

    master

    In Monocle, optics can be composed to navigate complex data structures. Most optics compose with any other optic. When an optic composes with itself, the resulting type remains the same.

    Key composition rules include:

    • Folds are the most general; they can be the result of composing almost any optic with another.
    • Lenses can compose with other Lenses to form a new Lens, or with Traversals to form a Traversal.
    • Prisms can compose with other Prisms to form a Prism, or with Lenses to form an Optional.
    • Isos are the most powerful, capable of composing into Lenses, Prisms, or other Isos.
  10. Compose Lenses together

    master

    Lenses can be composed to navigate through nested data structures or to chain modifications.

    Using andThen for deep zooming

    Use andThen to combine a lens that focuses on a field with a lens that focuses on a field within that field:

    val addressLens = GenLens[Person](_.address)
    val streetLens = GenLens[Address](_.streetName)
    
    addressLens.andThen(streetLens).get(john)

    Using compose for function composition

    You can compose the modification logic of multiple lenses using compose:

    import monocle.macros.syntax.lens._
    
    // Using syntax for a more fluent API
    john.lens(_.name).replace("Mike").lens(_.age).modify(_ + 1)

    Composing Lens with Prism for Optional fields

    If you need to update a Product type inside a Sum type (e.g., an Option), compose a Prism with a Lens using the some method:

    import monocle.macros.GenLens
    
    case class B(c: Int)
    case class A(b: Option[B])
    
    val c = GenLens[B](_.c)
    val b = GenLens[A](_.b)
    
    b.some.andThen(c).getOption(A(Some(B(1))))
    address.andThen(streetNumber).get(john)
    address.andThen(streetNumber).replace(2)(john)
  11. Difference between `at` and `index` optics

    master

    In Monocle, both at and index are used to define indexed optics, but they differ in their strength and capabilities:

    • index (an Optional): Can only update or access values that already exist at a specific key or position. It cannot insert new elements or delete existing ones. Because it is "weaker," it can be implemented for more data structures (e.g., List or Vector have Index but not At, because you cannot insert at an arbitrary index in a sequence).
    • at (a Lens): A "stronger" optic that allows for full CRUD-like operations on a key. It can update existing values, insert new elements (by replacing a missing key with Some(value)), and delete elements (by replacing an existing key with None).
    import monocle.Iso
    
    val m = Map("one" -> 1, "two" -> 2)
    val root = Iso.id[Map[String, Int]]
    
    // index: updates existing, no-op if missing
    root.index("two").replace(0)(m)   // update value at index "two"
    root.index("three").replace(3)(m) // noop because m doesn't have a value at "three"
    
    // at: insert, delete, or upsert
    root.at("three").replace(Some(3))(m)  // insert element at "three"
    root.at("two").replace(None)(m)       // delete element at "two"
    root.at("two").replace(Some(0))(m)    // upsert element at "two"
  12. Compare Monocle with other optics libraries

    master

    To understand the context of Monocle, you can explore these related libraries and their implementations of optics:

    • Haskell Lens: The library that served as the origin for Monocle.
    • Scalaz: Provides Lens and PLens (equivalent to Monocle's Optional).
    • Shapeless: Used for boilerplate-free Lens and Prism (what Shapeless calls Prism is called Optional in Monocle).
    • Quicklens: Another Scala-based lens library.