Зачем нужен Functor в scala кошек - PullRequest
1 голос
/ 30 мая 2020

Я только начал изучать фреймворк Scala cats. Я читаю Functor. Я разбирался в его особенностях, но не понимаю его использования. Зачем мне его использовать, если в Functors уже есть метод map, например List, Option et c.

Например,

val list = List(1, 2, 3)
Functor[List].map(list)(x => x * 2)

Но того же можно добиться с помощью

list.map(x => x * 2)

Что мы получим, когда абстрагируем метод map внутри Functor трейта. Может кто-нибудь пролить свет на это, чтобы я понял его использование.

Ответы [ 2 ]

3 голосов
/ 30 мая 2020

Вы можете вызвать .map для объекта, когда вы знаете, что этот объект имеет этот метод и что этот метод вызывается таким образом. Если вам известен точный тип объекта, компилятор может проверить, что это действительно так. Но что, если вы не знаете тип объекта? А что, если вы не хотите использовать отражение во время выполнения?

Представьте себе такую ​​ситуацию:

def doubleIntInF[F[_]: Functor](fInt: F[Int]): F[Int] =
  fInt.map(_ * 2)

Здесь мы не знаем тип F - это может быть List, Option, Future, IO, Either[String, *]. И все же мы можем добавить .map поверх него без использования отражения - мы используем Functor[F] для поддержки методов расширения. Мы могли бы сделать это и без метода расширения, например:

def doubleIntInF[F[_]: Functor](fInt: F[Int]): F[Int] =
  Functor[F].map(fInt)(_ * 2)

, и он будет работать (пока у нас есть правильные имплициты в области видимости):

doubleIntInF(List(1,2,3,4,5,6)) //  List(2, 4, 6, 8, 10, 12
doubleIntInF(Option(4)) // Some(2)

В случаях, когда мы знаем, что F = List, Option и т. д. c - у нас нет причин его использовать. Но у нас есть все причины для использования, если это F является динамическим c.

И зачем нам делать это F динамическим c? Чтобы использовать его в библиотеках, которые, например, могли бы объединить вместе несколько функций, предоставляемых через классы типов.

Например, если у вас есть F[_] и G[_] и у вас есть Traverse[F] и Applicative[G] (более мощные Functor ) вы можете превратить F[G[A]] в G[F[A]]:

val listOption: List[Option] = List(Some(1), Some(2))
listOption.sequence // Some(List(1, 2))

val listFuture: List[Option] = List(Future(1), Future(2))
listFuture.sequence // Future(List(1, 2))

Практически все библиотеки в экосистеме Cats используют эту концепцию (называемые классами типов) для реализации функциональности, не предполагая, что вы выбираете те же структуры данных и ввод-вывод. компоненты, как они бы. Пока вы можете предоставить экземпляры классов типов, которые доказывают, что они могут безопасно использовать некоторые методы для вашего типа, они могут реализовать эту функциональность (например, Cats Effect расширяет Cats некоторыми классами типов, а Doob ie, FS2, Http4s и т. Д. Строят помимо этого, не делая предположений о том, что вы используете для выполнения своих вычислений).

Короче говоря - в таких случаях, как ваш, нет смысла использовать Functor, но в целом они позволяют вам по-прежнему использовать .map в ситуациях, которые НЕ так просты и у вас нет жестко запрограммированного типа.

2 голосов
/ 30 мая 2020

Вопрос аналогичен тому, почему в OOP кому-то нужен интерфейс (черта), в то время как его реализации имеют те же методы.

Интерфейс - это абстракция. Абстракции полезны. При программировании мы предпочитаем сосредотачиваться на важных вещах и игнорировать детали, которые в настоящее время не важны. Также программирование с интерфейсами помогает разделить сущности, создавая лучшую архитектуру.

Интерфейсы (например, Comparable, Iterable, Serializable и c.) - это способ описания поведения в OOP. Классы типов (например, Functor, Monad, Show, Read et c.) - это способ описания поведения в FP.

Если вы просто хотите сопоставить (или flatMap) над списком (или Option) вам не нужно Functor. Если вы хотите работать со всеми отображаемыми элементами, которые вам нужны Functor, если вы хотите работать со всеми flatMap элементами, которые вам нужны Monad et c.

...