Как стать автором
Обновить
816.18
OTUS
Цифровые навыки от ведущих экспертов

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

Время на прочтение3 мин
Количество просмотров2.1K
Автор оригинала: Krzysztof Grajek

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

Теги:
Хабы:
+8
Комментарии1

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS