Контравариантный функтор в Scala Cats

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В этой статье мы поговорим о функторах. О функторах из библиотеки Cats, а не о классических функторах, которые мы все знаем и любим. Рассмотрим контравариантные функторы (Contravariant Functors), представленные в Cats в виде тайпкласса Contravariant.

Некоторые из вас, возможно, не знают, что классический функтор (Functor) с операцией map, который мы ежедневно используем в наших Scala Cats-проектах, на самом деле является ковариантным функтором (Covariant Functor). Также хочу отметить, что термин "Вариантность" (Variance) применительно к функторам не имеет ничего общего с различными видами вариативности, которые мы знаем, когда речь идет о типах и параметрическом полиморфизме.

Типичный функтор в терминах функционального программирования Scala представляет собой тайпкласс, оперирующий типами высших порядков (higher-kinded type), что оказывается весьма полезным, когда мы хотим абстрагироваться и обобщить наши API.

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

  def reverseStringOption(opt: Option[String]): Option[String] = opt.map(_.reverse)
  def reverseStringList(lst: List[String]): List[String] = lst.map(_.reverse)
  def reverseStringTry(t: Try[String]): Try[String] = t.map(_.reverse)

  //generalized version
  def reverse[F[_]](container: F[String])(implicit functor: Functor[F]): F[String] = 
    functor.map(container)(_.reverse)

Обобщенная версия reverse не зависит от типа обертки, которую мы хотим использовать, если в области видимости есть экземпляр Functor. В Cats это обычно происходит при импорте, например, Option:

import cats.instances.option._

Итак, я уверен, что вы все это уже знали. Функторы повсюду, и мы часто их используем. Аппликативы (Applicative) — это функторы, монады (Monad) — это функторы, даже простая функция с одним параметром (Function1) — это тоже функтор. Суть функтора (Functor) в методе map, преобразующим обернутое значение типа A в тип B с сохранением обертки.

def map[A, B](fa: F[A])(f: A => B): F[B]

Но хотите верьте, хотите нет, бывают случаи, когда мы хотим поменять местами типы в функции Functor f, чтобы она принимала тип B и возвращала тип A, но сохранила возвращаемую обертку для типа B — запутались? Если да, то читайте дальше — эта статья как раз для вас :).

Представьте себе простой тайпкласс, преобразующий некоторый тип T в Boolean. Определим его как простой трейт Filter:

 trait Filter[T] {
    def filter(value: T): Boolean
  }

Для работы с нашим тайпклассом создадим очень простой экземпляр и интерфейс:

implicit object StringFilter extends Filter[String] {
    override def filter(value: String): Boolean = value.length > 5
  }

  def filter[A](v: A)(implicit flt: Filter[A]) = flt.filter(v)

  def main(args: Array[String]): Unit = {
    println(filter("hello"))
    println(filter("hello world!"))
  }

Извините, что я не придумал здесь ничего более умного :) — реализация здесь не важна. Конечно, первый println выведет false, а второй — true.

Теперь представьте, что вам нужна функциональность функтора для тайпкласса Filter, чтобы преобразовать его из Filter[String] в Filter[Int] с помощью метода map:

  val simpleFilterFunctor = new Functor[Filter] {
    override def map[A, B](fa: Filter[A])(f: A => B): Filter[B] = new Filter[B] {
      override def filter(value: B): Boolean = ??? //fa.filter(f(value))
    }
  }

Видите ли вы проблему, с которой мы здесь столкнулись? Мы не можем передать значение в функцию f, поскольку нельзя использовать тип B в качестве входных данных для функции f. Здесь нам нужен тип A.

Другими словами, мы хотим получить A => Boolean из B => Boolean, имея функцию A => B.

Как это сделать? Нужно использовать контравариантный функтор вместо ковариантного. В Cats тайпкласс, предназначенный для этого, называется просто Contravariant.

  implicit val simpleFilterContravariant = new Contravariant[Filter] {
    override def contramap[A, B](fa: Filter[A])(f: B => A): Filter[B] = new Filter[B] {
      override def filter(value: B): Boolean = fa.filter(f(value))
    }
  }

Как вы, вероятно, заметили, тип входного параметра и тип выходного поменялись местами:

def map[A, B](initialValue: F[A])(f: A => B): F[B]
def contramap[A, B](fa: F[A])(f: B => A): F[B]

Композиция с контравариантным функтором (Contravariant) так же проста, как и с обычным ковариантным:

  //add companion object apply to easily instantiante filters
  object Filter {
    def apply[A](implicit instance: Filter[A]): Filter[A] = instance
  }

  //our existing filter (implicit defined earlier)
  val filterString = Filter[String]

  //our composed filter
  implicit val filterInt: Filter[Int] = Contravariant[Filter].contramap[String, Int](filterString)(_.toString)

  def main(args: Array[String]): Unit = {
    println(filter(3))
  }

Аналогично мы можем использовать Contravariant для работы с обернутыми значениями, например, в Option. Типичный пример с Show[_] из Cats:

  val showInts = Show[Int]
  implicit val showOption: Show[Option[Int]] = Contravariant[Show].contramap(showInts)(_.getOrElse(0))

  def main(args: Array[String]): Unit = {
    import cats.syntax.show._
    val x = Option(234)
    x.show
  }

или более кратко:

import cats.syntax.contravariant._
val showInts = Show[Int]
implicit val showOption: Show[Option[Int]] = showInts.contramap(_.getOrElse(0))

Таким образом, для фильтров мы можем использовать любые типы, обернутые в Option:

  implicit def filterOption[T](implicit flt: Filter[T]): Filter[Option[T]] = 
    simpleFilterContravariant.contramap(flt)(_.get)

  def main(args: Array[String]): Unit = {
    println(filter(Option("some string")))
  }

И избавиться от _.get:

  implicit def filterOption[T](implicit flt: Filter[T], m: Monoid[T]): Filter[Option[T]] =
    simpleFilterContravariant.contramap(flt)(_.getOrElse(m.empty))

  def main(args: Array[String]): Unit = {
    import cats.instances.string._
    println(filter(Option("some string")))
  }

Это все, что касается контравариантных функторов. Надеюсь, что эта тема стала вам более понятна и вы добавили еще один тайпкласс в свой инструментарий. Конечно, в нашей любимой библиотеке Cats есть множество других интересных тайпклассов и даже еще один интересный Functor :), но это я оставлю на потом.


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

Всех желающих приглашаем на открытый урок «Разработка простого REST API c помощью HTTP4S и ZIO». На примере построения простого веб сервиса с REST API, разберем основные компоненты (пути, бизнес логика, доступ к данным, документация), а также посмотрим как дружат такие функциональные библиотеки, как http4s, cats, zio в рамках одного приложения. РЕГИСТРАЦИЯ

Источник: https://habr.com/ru/company/otus/blog/568096/


Интересные статьи

Интересные статьи

Scala 3 — это новая основная версия языка программирования Scala. Это результат многолетних исследований, разработок и сотрудничества между компаниями и организациями, ко...
Одна из самых важных (на мой взгляд) функций в Битрикс24 это бизнес-процессы. Теоретически они позволяют вам полностью избавиться от бумажных служебок и перенести их в эл...
Предыстория Когда-то у меня возникла необходимость проверять наличие неотправленных сообщений в «1С-Битрикс: Управление сайтом» (далее Битрикс) и получать уведомления об этом. Пробле...
Реализация ORM в ядре D7 — очередная интересная, перспективная, но как обычно плохо документированная разработка от 1с-Битрикс :) Призвана она абстрагировать разработчика от механики работы с табл...
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?