Share
Embed
A Fistful of Functors
Functors show up everywhere in our day-to-day programming. They're so common, we take them for granted – especially in typed functional programming. Besides being common, they're incredibly useful for code reuse. However, functors have some lesser known variants: profunctors, bifunctors, contravariant functors, and so on. Guess what? They're amazingly useful, especially combined with other abstractions in the functional programming toolkit! In this talk, you'll discover the many species of functors and see how they can help you with tasks such as serialisation, stream processing, and more.
Itamar Ravid
December 13, 2018
Transcript
81 slides
-
A Fistful of Functors Itamar Ravid Scala Exchange 2018 Itamar
Ravid - @itrvd - #scalaX 1 -
The map function Everyone loves the map function! val list:
List[Int] = List(1, 2, 3) val mapped: List[String] = list.map(_.toString) Itamar Ravid - @itrvd - #scalaX 2 -
The map function Everyone loves the map function! val option:
Option[Int] = Some(1) val mapped: Option[String] = option.map(_.toString) Itamar Ravid - @itrvd - #scalaX 3 -
The map function Everyone loves the map function! val tuple:
(String, Int) = ("something", 1) val mapped: (String, String) = tuple.map(_.toString) Itamar Ravid - @itrvd - #scalaX 4 -
The map function Everyone loves the map function! import cats.effect.IO
val io: IO[Int] val mapped: IO[String] = io.map(_.toString) Itamar Ravid - @itrvd - #scalaX 5 -
Derived combinators We also get some cool derived functions for
free: def fproduct[A, B](f: A => B): IO[(A, B)] val result: IO[(Int, String)] = io.fproduct(_.toString) Itamar Ravid - @itrvd - #scalaX 6 -
Derived combinators We also get some cool derived functions for
free: def as[A, B](b: B): IO[B] val result: IO[String] = io.as("Replaced value") Itamar Ravid - @itrvd - #scalaX 7 -
Derived combinators We also get some cool derived functions for
free: def void: IO[Unit] val result: IO[Unit] = io.void Itamar Ravid - @itrvd - #scalaX 8 -
Functor The map function comes from the Functor typeclass: trait
Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] } // Laws: fa.map(identity) <-> fa fa.map(f).map(g) <-> fa.map(f andThen g) Itamar Ravid - @itrvd - #scalaX 9 -
Deceitful functors The laws help us root out the bad
seeds: val badOptionFunctor = new Functor[Option] { def map[A, B](fa: Option[A])(f: A => B): Option[B] = None } • Composition: ! • Identity: " Itamar Ravid - @itrvd - #scalaX 10 -
The essence of a functor Are functors just about replacing
the value? def map[A, B](fa: F[A])(f: A => B): F[B] Itamar Ravid - @itrvd - #scalaX 11 -
The essence of a functor Are functors just about replacing
the value? def map[A, B](fa: F[A])(f: A => B): F[B] They are, but there's more! Itamar Ravid - @itrvd - #scalaX 12 -
The essence of a functor Are functors just about replacing
the value? def map[A, B](f: A => B): F[A] => F[B] They are, but there's more! Itamar Ravid - @itrvd - #scalaX 13 -
The essence of a functor Are functors just about replacing
the value? def map[A, B](f: A => B): F[A] => F[B] They are, but there's more! Give me a plain function, I'll give you a fancy one. Itamar Ravid - @itrvd - #scalaX 14 -
The essence of a functor Let's use a Parser as
an example. Here's the definition: type Parser[A] = String => Option[A] Itamar Ravid - @itrvd - #scalaX 15 -
The essence of a functor And here's a Person parser:
case class Person(name: String, age: Int) val personParser: Parser[Person] = _.split(",") match { case Array(name, age) => Some(Person(name, age.toInt)) case _ => None } Itamar Ravid - @itrvd - #scalaX 16 -
The essence of a functor val nameParser: Parser[String] = personParser.map(_.name)
map for this personParser means that: • if you know how to transform persons to names (Person => String) Itamar Ravid - @itrvd - #scalaX 17 -
The essence of a functor val nameParser: Parser[String] = personParser.map(_.name)
map for this personParser means that: • if you know how to transform persons to names (Person => String) • you know, free of charge, to transform person parsers to name parsers (Parser[Person] => Parser[String]) Itamar Ravid - @itrvd - #scalaX 18 -
The essence of a functor trait Functor[F[_]] { def map[A,
B](fa: F[A])(f: A => B): F[B] } This is actually a covariant functor. Itamar Ravid - @itrvd - #scalaX 19 -
Producers of values Covariant functors produce values. val parse: Parser[Int]
// produces an int val opt: Option[Int] // might produce an int val list: List[Int] // produces zero or more ints Itamar Ravid - @itrvd - #scalaX 20 -
Producers of values Covariant functors produce values. val parse: Parser[Int]
// produces an int val opt: Option[Int] // might produce an int val list: List[Int] // produces zero or more ints Let's look at the dual of a producer. Itamar Ravid - @itrvd - #scalaX 21 -
Consumers of values Here's a predicate function: type Predicate[A] =
A => Boolean Itamar Ravid - @itrvd - #scalaX 22 -
Consumers of values And an integer predicate: val predicate: Predicate[Int]
= _ % 2 == 0 Itamar Ravid - @itrvd - #scalaX 23 -
Consumers of values And an integer predicate: val predicate: Predicate[Int]
= _ % 2 == 0 And an integer rendering function: val render: Int => String = _.toString Itamar Ravid - @itrvd - #scalaX 24 -
Consumers of values And an integer predicate: val predicate: Predicate[Int]
= _ % 2 == 0 And an integer rendering function: val render: Int => String = _.toString Can we compose them? Itamar Ravid - @itrvd - #scalaX 25 -
Consumers of values We can try composing with a producer:
Itamar Ravid - @itrvd - #scalaX 28 -
Consumers of values Which in code is: val predicate: Predicate[Int]
= _ % 2 == 0 val produce: String => Int = _.toInt val composed: Predicate[String] = produce andThen pred Itamar Ravid - @itrvd - #scalaX 29 -
Consumers of values Interesting! Predicate[Int] looks like a functor, produce:
String => Int acted as the mapping function, but the types are backwards. Itamar Ravid - @itrvd - #scalaX 30 -
Contravariant functors Contravariant functors are exactly these "backward" functors: trait
Contravariant[F[_]] { def contramap[A, B](fa: F[A])(f: B => A): F[B] } Itamar Ravid - @itrvd - #scalaX 31 -
Contravariant functors Contravariant functors are exactly these "backward" functors: trait
Contravariant[F[_]] { def contramap[A, B](fa: F[A])(f: B => A): F[B] } Contrast with the covariant functor: def contramap[A, B](fa: F[A])(f: B => A): F[B] def map[A, B](fa: F[A])(f: A => B): F[B] Itamar Ravid - @itrvd - #scalaX 32 -
Contravariant functors And with the rearranged form: def contramap[A, B](f:
B => A): F[A] => F[B] def map[A, B](f: A => B): F[A] => F[B] Itamar Ravid - @itrvd - #scalaX 33 -
Contravariant functors Can't forget the laws! trait Contravariant[F[_]] { def
contramap[A, B](fa: F[A])(f: B => A): F[B] } fa.contramap(identity) <-> fa fa.contramap(f).contramap(g) <-> fa.contramap(g andThen f) Itamar Ravid - @itrvd - #scalaX 34 -
Contravariant functors So in our case, we could say: val
predicate: Predicate[Int] = _ % 2 == 0 val produce: String => Int = _.toInt val composed: Predicate[String] = predicate contramap produce Itamar Ravid - @itrvd - #scalaX 35 -
Contravariant functors in the wild Anything that looks like a
consuming function: type Encoder[A] = A => String type Ordering[A] = (A, A) => ComparisonResult type LeftFold[S, A] = (S, A) => S Itamar Ravid - @itrvd - #scalaX 36 -
What's the point? To adapt a consumer, you just need
a plain function! // ZIO Streams' Sink trait Sink[E, A0, A, R] val sum: Sink[Nothing, Nothing, Int, Int] = Sink.fold(0)((acc, el) => Sink.Step.more(acc + el)) case class Person(name: String, salary: Int) val salarySum: Sink[Nothing, Nothing, Person, Int] = sum.contramap(_.salary) Itamar Ravid - @itrvd - #scalaX 37 -
A conundrum Is this function a contravariant or covariant functor?
type Semigroup[A] = (A, A) => A Itamar Ravid - @itrvd - #scalaX 38 -
A conundrum Is this function a contravariant or covariant functor?
type Semigroup[A] = (A, A) => A Both! Itamar Ravid - @itrvd - #scalaX 39 -
Invariant functor trait InvariantFunctor[F[_]] { def imap(fa: F[A])(f: A =>
B)(g: B => A): F[B] } // Laws: fa.imap(identity)(identity) <-> fa fa.imap(f1)(g1).imap(f2)(g2) <-> fa.imap(f1 andThen f2)(g2 andThen g1) Itamar Ravid - @itrvd - #scalaX 40 -
Invariant functor So let's say I have another newtype: case
class Age(age: Int) And I need a Semigroup for it. Itamar Ravid - @itrvd - #scalaX 41 -
Invariant functor And we can use imap to get Semigroup[Age]:
Itamar Ravid - @itrvd - #scalaX 43 -
Invariant functor In code, this looks like: val intSemigroup: Semigroup[Int]
= _ + _ case class Age(age: Int) val ageSemigroup = intSemigroup.imap(Age(_))(_.age) Itamar Ravid - @itrvd - #scalaX 44 -
Functor composition An interesting thing about functors is that they
compose: If F[_] is a Functor, and G[_] is a Functor, then F[G[_]] is a Functor too. Itamar Ravid - @itrvd - #scalaX 45 -
Functor composition To give a few examples: IO[Option[Either[String, Person]]] List[Predicate[String]]
Predicate[List[String]] Itamar Ravid - @itrvd - #scalaX 46 -
Functor composition But which functor? These work like + and
-: Itamar Ravid - @itrvd - #scalaX 47 -
Functor composition What can we do by composing (just) functors?
import cats.data.Nested val nestedThing: IO[Option[Either[String, Person]]] val name: IO[Option[Either[String, String]]] = Nested(Nested(nestedThing)) .map(_.name) .value .value Itamar Ravid - @itrvd - #scalaX 48 -
Functor composition What can we do by composing (just) functors?
val predicates: List[Predicate[String]] val personPredicates: List[Predicate[Person]] = Nested[List, Predicate, String](predicates) .contramap(_.name) .value Itamar Ravid - @itrvd - #scalaX 49 -
Functor composition What can we do by composing (just) functors?
val stringsPredicate: Predicate[List[String]] val personsPredicate: Predicate[List[Person]] = Nested[Predicate, List, String](stringsPredicate) .contramap(_.name) .value Itamar Ravid - @itrvd - #scalaX 50 -
Functor composition And these are just functors! Itamar Ravid -
@itrvd - #scalaX 51 -
Mapping to the left Let's recall Either: sealed abstract class
Either[L, R] case class Right[L, R](r: R) extends Either[L, R] case class Left[L, R](l: L) extends Either[L, R] The covariant functor maps the right value. What about the left value? Itamar Ravid - @itrvd - #scalaX 52 -
Mapping to the left case class Error(msg: String) case class
CompositeError(errors: Error*) // have: val result: Either[Error, Result] // want: val result: Either[CompositeError, Result] Itamar Ravid - @itrvd - #scalaX 53 -
Mapping to the left We can map the left side
using leftMap: val lmapped: Either[CompositeError, Result] = result.leftMap(CompositeError(_)) So Either behaves covariantly in L and R. But we need to distinguish between them. Itamar Ravid - @itrvd - #scalaX 54 -
Bifunctor Covariant functors with two slots are called Bifunctors: trait
Bifunctor[F[_, _]] { def bimap[A, B, C, D](fa: F[A, B])(f: A => C, g: B => D): F[C, D] } // Laws: fa.bimap(identity, identity) <-> fa fa.bimap(f1, g1).bimap(f2, g2) <-> fa.bimap(f1 andThen f2, g1 andThen g2) Itamar Ravid - @itrvd - #scalaX 55 -
Bifunctor And Either has an instance of Bifunctor: def bimap[A,
B, C, D](fa: Either[A, B])(f: A => C, g: B => D): Either[C, D] = fa match { case Left(l) => Left(f(l)) case Right(r) => Right(g(r)) } Itamar Ravid - @itrvd - #scalaX 56 -
Bifunctors in the wild Apart from Either, the plain Tuple
also forms a Bifunctor: def bimap[A, B, C, D](fa: (A, B))(f: A => C, g: B => D): (C, D) = (f(fa._1), g(fa._2)) But why should you care? Itamar Ravid - @itrvd - #scalaX 57 -
Bifunctor composition Bifunctors can also compose! type TwoOptions[A, B] =
(Option[A], Option[B]) This is a Bifunctor composed on a Functor. Itamar Ravid - @itrvd - #scalaX 58 -
Bifunctor composition Bifunctors can also compose! type ListOfTuples[A, B] =
List[(A, B)] And this is a Functor composed on a Bifunctor. Itamar Ravid - @itrvd - #scalaX 59 -
Bifunctor composition Bifunctors can also compose! type TwoOptions[A, B] =
(Option[A], Option[B]) type ListOfTuples[A, B] = List[(A, B)] They both form a Bifunctor. Itamar Ravid - @itrvd - #scalaX 60 -
Bifunctor composition Bifunctors can also compose! type TwoOptions[A, B] =
(Option[A], Option[B]) type ListOfTuples[A, B] = List[(A, B)] They both form a Bifunctor. Kmett calls them Biff (for Bifunctor-functor-functor), and Tannen in Bifunctors. Itamar Ravid - @itrvd - #scalaX 61 -
Bifunctor composition With Binested in cats, we can compose a
functor on a bifunctor: val l: List[(Person, Age)] val bimapped: List[(String, Int)] = Binested(l).bimap(_.name, _.value).value Itamar Ravid - @itrvd - #scalaX 62 -
Bifunctor composition With Bitraversable, you get more fancy tricks: val
twoOpts: (Option[String], Option[Int]) val flipped: Option[(Int, String)] = twoOpts.bisequence Itamar Ravid - @itrvd - #scalaX 63 -
Functions Let's look at this fancy type: type Func[A, B]
= A => B Would you say this is a Bifunctor? Itamar Ravid - @itrvd - #scalaX 64 -
Functions Let's look at this fancy type: type Func[A, B]
= A => B It has two slots like a Bifunctor, but we've seen that functions are contravariant in the input. Itamar Ravid - @itrvd - #scalaX 65 -
Profunctor A Profunctor is a type with two slots -
contravariant and covariant: trait Profunctor[F[_, _]] { def dimap[A, B, C, D](fa: F[A, B])(f: C => A, g: B => D): F[C, D] } Itamar Ravid - @itrvd - #scalaX 66 -
Profunctor A Profunctor is a type with two slots -
contravariant and covariant: trait Profunctor[F[_, _]] { def dimap[A, B, C, D](fa: F[A, B])(f: C => A, g: B => D): F[C, D] } Constrast this with the Bifunctor signature: def dimap[A, B, C, D](fa: F[A, B])(f: C => A, g: B => D): F[C, D] def bimap[A, B, C, D](fa: F[A, B])(f: A => C, g: B => D): F[C, D] Itamar Ravid - @itrvd - #scalaX 67 -
Profunctor And the laws: trait Profunctor[F[_, _]] { def dimap[A,
B, C, D](fa: F[A, B])(f: C => A, g: B => D): F[C, D] } // Laws fa.dimap(identity, identity) <-> fa fa.dimap(f1, g1).dimap(f2, g2) <-> fa.dimap(f2 andThen f1, g1 andThen g2) Itamar Ravid - @itrvd - #scalaX 68 -
Profunctor You could say that a Profunctor is a generalized
function: Itamar Ravid - @itrvd - #scalaX 69 -
Profunctor You could say that a Profunctor is a generalized
function: Itamar Ravid - @itrvd - #scalaX 70 -
Profunctors in the wild The plain function is a Profunctor.
Anything cooler? Itamar Ravid - @itrvd - #scalaX 71 -
Profunctors in the wild Of course! ZIO Streams' Sink is
a profunctor: trait Sink[E, A0, A, R] The profunctor is constructed on the A and R parameters. Itamar Ravid - @itrvd - #scalaX 72 -
Profunctors in the wild To see this in action, let's
go back to our summing sink: val sum: Sink[Nothing, Nothing, Int, Int] = Sink.fold(0)((acc, el) => Sink.Step.more(acc + el)) Itamar Ravid - @itrvd - #scalaX 73 -
Profunctors in the wild To see this in action, let's
go back to our summing sink: val sum: Sink[Nothing, Nothing, Int, Int] = Sink.fold(0)((acc, el) => Sink.Step.more(acc + el)) We can adapt it to sum salaries and format the result: val salaries: Sink[Nothing, Nothing, Person, String] = sum.dimap(_.salary, res => s"The result is $res") Itamar Ravid - @itrvd - #scalaX 74 -
Profunctor composition Unsurprisingly, Profunctors also compose. We can compose another
Functor in one or both slots: type Output[A, B] = A => List[B] Itamar Ravid - @itrvd - #scalaX 75 -
Profunctor composition Unsurprisingly, Profunctors also compose. We can compose another
Functor in one or both slots: type Output[A, B] = A => List[B] (yes, this is a Kleisli arrow!) Itamar Ravid - @itrvd - #scalaX 76 -
Profunctor composition Unsurprisingly, Profunctors also compose. We can compose another
Functor in one or both slots: type Input[A, B] = Option[A] => B Itamar Ravid - @itrvd - #scalaX 77 -
Profunctor composition Unsurprisingly, Profunctors also compose. We can compose another
Functor in one or both slots: type Both[A, B] = Option[A] => List[B] The Profunctor instance let's us "forget" about the input and output effects. Itamar Ravid - @itrvd - #scalaX 78 -
Thank you! Questions? The video will be available soon here!
Itamar Ravid - @itrvd - #scalaX 81