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

от автора

В этой статье мы поговорим о функторах. О функторах из библиотеки 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *