Introduction to Optics in Scala

Throughout this blog I have repeatedly mentioned the benefits of a strong type system. I talked about type refinement to check valuesO designed for beginners And advanced approach to type-class derivation or a type-safe approach to messaging using pass4s.

However, our capabilities do not end there, so today I want to tell you about one more topic that those new to languages ​​like Scala or Haskell may not know about.

In this post I will talk about optics.

Introduction to Optics

Optics is a collective concept that combines access to immutable data and its transformations. We will explore the world of optics using the library Monocle. But before we get started, let's prepare a springboard and look at the problems we can solve.

Subject area

Imagine we are modeling data for a supermarket. Products with id, name And price, stored on shelves, grouped in display cases, and so on. Our goal is to model this entire complex structure.

supermarket shelves

shelves with products

Let's start with the simplest thing – with the product:

case class Product(id: String, name: String, price: Double)

Products are stored on the shelf:

case class Shelf(id: String, product: Product)

Shelves allow you to build a stand with goods:

case class Display(id: String, kind: "Ambient" | "Chilled", shelves: List[Shelf])

Stands with goods form a passage:

case class Alley(id: String, displays: List[Display])

The store is just a lot of aisles:

case class Shop(alleys: List[Alley])

Task

Our task looks quite simple – we want to apply a 10% discount on assortment throughout the store. The entire store remains the same, only the prices change. Have you figured out how to do it the old fashioned way? Most likely using a heap map and copy. Try it yourself as an exercise!

As test data, let's assume our store looks like this:

val shop = Shop(
  alleys = List(
    Alley(
      id = "1",
      displays = List(
        Display("1", "Ambient", List(Shelf("1", water), Shelf("2", milk))),
        Display("2", "Chilled", List(Shelf("3", cheese), Shelf("4", ham)))
      )
    )
  )
)

We solve this problem using optics

Let's start with the simplest and limit ourselves to some subset of products, for example, one shelf:

val shelf = Shelf("1", water)

In our solution we will use scala-cli. Let's start by installing the dependencies and including the syntax from Monocle.

//> using scala "3.3.0"
//> using dep "dev.optics::monocle-core:3.2.0"
//> using dep "dev.optics::monocle-macro:3.2.0"

import monocle.syntax.all._

Thanks to the import of this syntax, all case classes are now equipped with optical manipulators. With their help, applying a discount to the entire shelf at once becomes easy:

shelf
  .focus(_.product.price)
  .modify(_ * 0.9)

The focus explanatory note indicates how the optics go to the price. We then apply the discount by calling modify.

Now that we know the basics, let's apply the discount to the entire store assortment at once, as we wanted from the very beginning:

val discounted = 
  shop
    .focus(_.alleys)
    .each
    .refocus(_.displays)
    .each
    .refocus(_.shelves)
    .each
    .refocus(_.product.price)
    .modify(_ * 0.9)

As you can see, we are simply repeating focus And each. They mean that we are focusing on a field like List, and then instruct the optics to apply the upcoming combinators to each of the elements. It feels like work map or forEach. Then, when we finally get to the shelf, all we have to do is apply the logic we discussed earlier.

Conclusion

This completes the introduction! There's much more to Monocle's functionality, and we've only scratched the surface. There are a whole host of other useful applications. Personally, I find optics extremely useful for manipulating data in integration tests, where you need to see how large data structures are modified by entire software components.

Want to know more? Check out the Monocle documentation https://www.optics.dev/Monocle/.

Want to see the full code example? It's in gist https://gist.github.com/majk-p/dfdcf08bdfc3986c3dcd94cc02fa4f52 – you can always run it using scala-cli.

The translation was prepared as part of the launch of a new stream on Scala developer course.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *