На протяжении всего этого блога я неоднократно упоминал о преимуществах сильной системы типов. Я рассказывал об уточнении типов (type refinement) для проверки значений, о рассчитанном на новичков и продвинутом подходе к выведению классов типов (type-class derivation) или о типобезопасном подходе к обмену сообщениями с помощью pass4s.

Однако этим наши возможности не исчерпываются, поэтому сегодня я хочу рассказать вам еще об одной теме, о которой новички в таких языках, как Scala или Haskell, могут не знать.

В этом посте я расскажу об оптике.

Знакомство с оптикой

Оптика — это собирательное понятие, объединяющее доступ к неизменяемым данным и их преобразованиям. Мы будем исследовать мир оптики с помощью библиотеки Monocle. Но прежде чем приступить к работе, давайте подготовим плацдарм и рассмотрим задачи, которые мы можем решить.

Предметная область

Представьте, что мы моделируем данные для супермаркета. Товары с id, name и price, хранящиеся на полках, сгруппированные в витрины и так далее. Наша цель — смоделировать всю эту сложную структуру.

полки с продуктами

Начнем с самого простого — с продукта:

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

Продукты хранятся на полке:

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

Полки позволяют выстраивать стенд с товарами:

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

Стенды с товарами формируют проход:

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

Магазин — это просто множество проходов:

case class Shop(alleys: List[Alley])

Задача

Наша задача выглядит довольно просто — мы хотим применить скидку в 10% на ассортимент во всем магазине. Весь магазин остается прежним, меняются только цены. Уже представили, как это сделать по старинке? Скорее всего с помощью кучи map и copy. Попробуйте сделать это сами в качестве упражнения!

В качестве тестовых данных предположим, что наш магазин выглядит следующим образом:

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)))
      )
    )
  )
)

Решаем эту задачу с помощью оптики

Давайте начнем с самого простого и ограничимся каким-нибудь подмножеством товаров, например, одной полкой:

val shelf = Shelf("1", water)

В нашем решении мы будем использовать scala-cli. Начнем с установки зависимостей и включения синтаксиса из 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._

Благодаря импорту этого синтаксиса все case class‘ы теперь дополнены оптическими манипуляторами. С их помощью применить скидку сразу ко всей полке становится проще простого:

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

Макрос focus указывает, как найти поле price. Затем мы применяем скидку, вызывая modify.

Теперь, когда мы знаем основы, давайте применим скидку сразу ко всему ассортименту магазина, как мы хотели с самого начала:

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

Как видите, мы просто повторяем focus и each. Они означают, что мы фокусируемся на поле типа List, а затем даем указания оптике применить предстоящие комбинаторы к каждому из элементов. Это напоминает работу map или forEach. Затем, когда мы наконец добрались до полки, нам остается только применить логику, которую мы уже обсудили раньше.

Заключение

На этом введение закончено! Функциональность Monocle гораздо шире, и мы только скользнули по поверхности. Существует еще целое множество других полезных примененийю. Лично я нахожу оптику чрезвычайно полезной для манипулирования данными в интеграционных тестах, когда нужно увидеть, как большие структуры данных изменяются целыми программными компонентами.

Хотите узнать больше? Ознакомьтесь с документацией Monocle https://www.optics.dev/Monocle/.

Хотите увидеть полный пример кода? Он находится в gist https://gist.github.com/majk-p/dfdcf08bdfc3986c3dcd94cc02fa4f52 — вы всегда можете запустить его с помощью scala-cli.

Перевод подготовлен в рамках старта нового потока на курс «Scala-разработчик».