Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру 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 в рамках одного приложения. РЕГИСТРАЦИЯ