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