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

от автора

Сегодня поговорим о еще одном функторе — инвариантном (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/


Комментарии

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

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