
Сегодня поговорим о еще одном функторе — инвариантном (Invariant Functor). Уже было несколько постов о ковариантных функторах (называемых просто «функторами») и контравариантных функторах. Если концепция ковариантных и контравариантных функторов вам понятна, то с инвариантным все будет просто — он сочетает в себе функциональность обоих вышеупомянутых функторов.
Как вы помните, с помощью функторов мы можем отображать один тип в другой с помощью функции f:
trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] }
С контравариантными функторами мы работаем также, но меняем местами типы в f:
def contramap[A, B](fa: F[A])(f: B => A): F[B]
Это бывает полезно, когда нужно предоставить новые неявные реализации какого-то тайпкласса, повторно используя имеющиеся имплементации.
Короче говоря, map в функторе используется, если мы добавляем операции в конец последовательности, а contramap в контравариантном функторе, когда мы хотим добавить их в начало. Для инвариантного функтора есть еще третий вариант — imap, который позволяет работать в обоих направлениях.
Инвариантные функторы реализуют метод imap,который эквивалентен комбинации map и contramap. Метод imap — генерирует новые тайпклассы с помощью пары двунаправленных преобразований.
Самый простой и распространенный пример инвариантных функторов — это кодеки и парсеры, которые выполняют преобразование в обоих направлениях.
trait CustomParser[A] { def encode(value: A): String def decode(value: String): A def imap[B](dec: A => B, enc: B => A): CustomParser[B] = ??? } def encode[A](value: A)(implicit c: CustomParser[A]): String = c.encode(value) def decode[A](value: String)(implicit c: CustomParser[A]): A = c.decode(value)
В библиотеке Cats определение imap в инвариантном функторе выглядит аналогично:
def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]
Можно реализовать наш imap в терминах методов encode и decode:
def imap[B](dec: A => B, enc: B => A): CustomParser[B] = { val self = this new CustomParser[B] { def encode(value: B): String = self.encode(enc(value)) def decode(value: String): B = dec(self.decode(value)) } }
Таким образом, имея реализации методов encode/decode, мы можем предоставить imap, который позволит нам легко создавать новые экземпляры CustomParser для других типов:
implicit val longParser: CustomParser[Long] = new CustomParser[Long] { def encode(value: Long): String = value.toString def decode(value: String): Long = value.toLong }
Допустим, у нас есть реализация CustomParser для Long в нашей неявной области видимости, и мы хотим получить другой парсер для java.util.Date. Мы знаем, как преобразовать Date в Long и обратно, поэтому, используя идею инвариантного функтора, мы можем легко создать новый парсер:
implicit val dateParser: CustomParser[Date] = longParser.imap(new Date(_), _.getTime)
Это похоже на уже обсуждавшиеся ранее ковариантные и контравариантные функторы. Кстати, ковариантный и контравариантный функторы на самом деле являются потомками инвариантного функтора, другими словами, функция imap может быть реализована для них с помощью их map или contramap соответственно.
trait Invariant[F[_]] { def imap[A, B](fa: F[A])(dec: A => B)(enc: B => A): F[B] } trait Conravariant[F[_]] extends Invariant[F] { def contramap[A, B](fa: F[A])(enc: B => A): F[B] def imap[A, B](fa: F[A])(dec: A => B)(enc: B => A): F[B] = contramap(fa)(enc) } trait Covariant[F[_]] extends Invariant[F] { def map[A, B](fa: F[A])(enc: A => B): F[B] def imap[A, B](fa: F[A])(dec: A => B)(enc: B => A): F[B] = map(fa)(dec) }
Про Invariant из документации Cats:
Каждый ковариантный (а также контравариантный) функтор порождает инвариантный функтор, игнорируя функцию g (или, в случае контравариантности, f).
где f и g соответствуют нашим функциям dec и enc.
В документации Cats упоминается еще один хороший пример создания новых экземпляров Invariant с использованием тайпкласса Semigroup.
Используя Semigroup[Long] из Cats, мы можем легко добавлять новые экземпляры Semigroup для новых типов, если знаем, как преобразовать один тип в другой и обратно. В примере используются преобразования Long -> Date и Date -> Long, использованные ранее:
import cats._ import cats.implicits._ def longToDate: Long => Date = new Date(_) def dateToLong: Date => Long = _.getTime implicit val semigroupDate: Semigroup[Date] = Semigroup[Long].imap(longToDate)(dateToLong) val today: Date = longToDate(1449088684104l) val timeLeft: Date = longToDate(1900918893l) today |+| timeLeft // res1: Date = Thu Dec 24 21:40:02 CET 2015
Материал подготовлен в рамках курса «Scala-разработчик».
Всех желающих приглашаем на открытый урок «Разработка простого REST API c помощью HTTP4S и ZIO». На примере построения простого веб сервиса с REST API, разберем основные компоненты (пути, бизнес логика, доступ к данным, документация), а также посмотрим как дружат такие функциональные библиотеки, как http4s, cats, zio в рамках одного приложения. РЕГИСТРАЦИЯ
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/568118/
Добавить комментарий