
Сегодня поговорим о еще одном функторе — инвариантном (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 в рамках одного приложения. РЕГИСТРАЦИЯ