Upgrade to Pro — share decks privately, control downloads, hide ads and more …

A Fistful of Functors

Itamar Ravid
December 13, 2018

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
Tweet

More Decks by Itamar Ravid

Other Decks in Technology

Transcript

  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  21. Consumers of values Here's a predicate function: type Predicate[A] =

    A => Boolean Itamar Ravid - @itrvd - #scalaX 22
  22. Consumers of values And an integer predicate: val predicate: Predicate[Int]

    = _ % 2 == 0 Itamar Ravid - @itrvd - #scalaX 23
  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
  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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. A conundrum Is this function a contravariant or covariant functor?

    type Semigroup[A] = (A, A) => A Itamar Ravid - @itrvd - #scalaX 38
  35. A conundrum Is this function a contravariant or covariant functor?

    type Semigroup[A] = (A, A) => A Both! Itamar Ravid - @itrvd - #scalaX 39
  36. 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
  37. 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
  38. 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
  39. 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
  40. Functor composition But which functor? These work like + and

    -: Itamar Ravid - @itrvd - #scalaX 47
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. Profunctor You could say that a Profunctor is a generalized

    function: Itamar Ravid - @itrvd - #scalaX 69
  62. Profunctor You could say that a Profunctor is a generalized

    function: Itamar Ravid - @itrvd - #scalaX 70
  63. Profunctors in the wild The plain function is a Profunctor.

    Anything cooler? Itamar Ravid - @itrvd - #scalaX 71
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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