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. A Fistful of Functors
    Itamar Ravid
    Scala Exchange 2018
    Itamar Ravid - @itrvd - #scalaX 1

    View full-size slide

  2. 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

    View full-size slide

  3. 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

    View full-size slide

  4. 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

    View full-size slide

  5. 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

    View full-size slide

  6. 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

    View full-size slide

  7. 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

    View full-size slide

  8. 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

    View full-size slide

  9. 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

    View full-size slide

  10. 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

    View full-size slide

  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]
    Itamar Ravid - @itrvd - #scalaX 11

    View full-size slide

  12. 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

    View full-size slide

  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!
    Itamar Ravid - @itrvd - #scalaX 13

    View full-size slide

  14. 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

    View full-size slide

  15. 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

    View full-size slide

  16. 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

    View full-size slide

  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)
    Itamar Ravid - @itrvd - #scalaX 17

    View full-size slide

  18. 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

    View full-size slide

  19. 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

    View full-size slide

  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
    Itamar Ravid - @itrvd - #scalaX 20

    View full-size slide

  21. 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

    View full-size slide

  22. Consumers of values
    Here's a predicate function:
    type Predicate[A] = A => Boolean
    Itamar Ravid - @itrvd - #scalaX 22

    View full-size slide

  23. Consumers of values
    And an integer predicate:
    val predicate: Predicate[Int] = _ % 2 == 0
    Itamar Ravid - @itrvd - #scalaX 23

    View full-size slide

  24. 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

    View full-size slide

  25. 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

    View full-size slide

  26. Consumers of values
    Itamar Ravid - @itrvd - #scalaX 26

    View full-size slide

  27. Consumers of values
    Itamar Ravid - @itrvd - #scalaX 27

    View full-size slide

  28. Consumers of values
    We can try composing with a producer:
    Itamar Ravid - @itrvd - #scalaX 28

    View full-size slide

  29. 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

    View full-size slide

  30. 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

    View full-size slide

  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]
    }
    Itamar Ravid - @itrvd - #scalaX 31

    View full-size slide

  32. 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

    View full-size slide

  33. 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

    View full-size slide

  34. 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

    View full-size slide

  35. 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

    View full-size slide

  36. 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

    View full-size slide

  37. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  40. 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

    View full-size slide

  41. 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

    View full-size slide

  42. Invariant functor
    Here's our Semigroup[Int]:
    Itamar Ravid - @itrvd - #scalaX 42

    View full-size slide

  43. Invariant functor
    And we can use imap to get Semigroup[Age]:
    Itamar Ravid - @itrvd - #scalaX 43

    View full-size slide

  44. 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

    View full-size slide

  45. 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

    View full-size slide

  46. Functor composition
    To give a few examples:
    IO[Option[Either[String, Person]]]
    List[Predicate[String]]
    Predicate[List[String]]
    Itamar Ravid - @itrvd - #scalaX 46

    View full-size slide

  47. Functor composition
    But which functor? These work like + and -:
    Itamar Ravid - @itrvd - #scalaX 47

    View full-size slide

  48. 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

    View full-size slide

  49. 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

    View full-size slide

  50. 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

    View full-size slide

  51. Functor composition
    And these are just functors!
    Itamar Ravid - @itrvd - #scalaX 51

    View full-size slide

  52. 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

    View full-size slide

  53. 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

    View full-size slide

  54. 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

    View full-size slide

  55. 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

    View full-size slide

  56. 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

    View full-size slide

  57. 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

    View full-size slide

  58. 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

    View full-size slide

  59. 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

    View full-size slide

  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.
    Itamar Ravid - @itrvd - #scalaX 60

    View full-size slide

  61. 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

    View full-size slide

  62. 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

    View full-size slide

  63. 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

    View full-size slide

  64. 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

    View full-size slide

  65. 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

    View full-size slide

  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]
    }
    Itamar Ravid - @itrvd - #scalaX 66

    View full-size slide

  67. 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

    View full-size slide

  68. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  71. Profunctors in the wild
    The plain function is a Profunctor. Anything cooler?
    Itamar Ravid - @itrvd - #scalaX 71

    View full-size slide

  72. 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

    View full-size slide

  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))
    Itamar Ravid - @itrvd - #scalaX 73

    View full-size slide

  74. 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

    View full-size slide

  75. 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

    View full-size slide

  76. 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

    View full-size slide

  77. 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

    View full-size slide

  78. 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

    View full-size slide

  79. Summary
    Itamar Ravid - @itrvd - #scalaX 79

    View full-size slide

  80. Summary
    Source: https://github.com/tpolecat/cats-infographic by Rob Norris 80

    View full-size slide

  81. Thank you!
    Questions?
    The video will be available soon here!
    Itamar Ravid - @itrvd - #scalaX 81

    View full-size slide