Skip to content

Instantly share code, notes, and snippets.

@jisantuc jisantuc/typeclasses-and-variance.md Secret
Last active Mar 30, 2020

Embed
What would you like to do?
title author patat
Typeclasses and Variance
James Santucci
wrap margins
true
left right
40
40

Typeclasses

General form

  • Things with the shape F[_]
  • That are used as: TYPE -> CONSTRAINT
  • CONSTRAINT constructors, without inheritance

Why are they good

  • Let us define interfaces --
trait CsvEncoder[T] {
  def encodeRow(row: T): CsvRow = ???
}
  • Let us define standard interfaces for common operations
trait Functor[F[_]] {
  def map[A, B](f: A => B)(fa: F[A]): F[B] = ???
}
  • In ways that don't mess with OOP challenges! i.e., no diamond problem
  • Also, many typeclasses have laws, and we can test those laws in different ways. In Scala, we use Discipline

Some common typeclasses

Semigroup

  • Semigroup is for things that you can combine
trait Semigroup[T] {
  def combine(thisOne: T, thatOne: T): T
}
  • Laws: associativity
combine(combine(x, y), z) == combine(x, combine(y, z))

Monoid

  • Monoid is for things you can combine that also have an identity.
  • Having a Monoid implies having a Semigroup
trait Monoid[T] {
  def combine(thisOne: T, thatOne: T): T
  def empty: T
}
  • Laws: associativity (as above), left and right identity:
// for some type T
combine(x, Monoid[T].empty) == combine(Monoid[T].empty, x) == x

What do these have in common

  • Extra capabilities for some concrete type
  • T, not F[_]
  • T is a type, while F[_] is a type constructor
  • typeclasses of T will define things we can do with values. typeclasses of F[_] will change how we can combine and compose values in contexts
    • Some contexts: eventuality (Future, IO), optionality (Option), potential for failure (Try, Either)
    • Typeclasses on higher-kinded types let us express common operations over a range of contexts

Typeclasses with higher-kinded type parameters

Functor

  • For contexts where we can lift a function of one argument into the context
trait Functor[F[_]] {
  def map[A, B](f: A => B)(fa: F[A]): F[B] = ???
}
  • laws: identity, composition
// identity
map(identity)(fa) == fa

// composition
// harder to express succinctly in Scala, but basically:
// composing then lifting should result in the same thing as composing
// lifted functions

Applicative

  • For contexts where we want to combine several values in context
  • Functor gave us a map, but what is the return type in this case?
def f(x: Int, y: Int): Int = ???

// pretend this is partial application
Option(3).map(f)
  • So how do we apply f in context?
(Option(3), None).mapN(f)
(Option(3), Option(4)).mapN(f)

Applicative

  • what's happening?
  • as long as F[_] has an applicative, we call functions of n parameters on n instances of F[_], provided the function arguments align with the wrapped types in F
trait Applicative[F[_]] extends Functor[F] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]

  def pure[A](a: A): F[A]
}
  • product and a syntax method .tupled are what let us switch between tuples of F[_] and F of a tuple

Monads

  • for sequential composition in context
trait Monad[F[_]] extends Applicative[F] {
  def flatMap[A, B](f: A => F[B])(fa: F[A]): F[B]
}

Monads

  • power for comprehension syntax -- for comprehensions desugar to chained flatMaps
@ desugar {
    for {
      x <- Option(4)
      y <- Option(5)
      z <- Option.empty[Int]
    } yield x + y + z
  }
res0: Desugared =
  Option[Int](4)
    .flatMap(((x: Int) => Option[Int](5)
	  .flatMap(((y: Int) => None.map(((z: Int) => x.+(y).+(z)))))))
  • for short-circuiting computations in context

Traversable

  • for transforming context... IN CONTEXT
trait Traversable[F[_]] {
  def traverse[G[_], A, B](f: A => G[B])(fa: F[A]): G[F[B]]
  def sequence[G[_], A](fga: F[G[A]]): G[F[A]]
}
  • a trick (always be suspicious if you find yourself needing to .sequence):
def f[G[_], A](a: A): G[A] = ???

fa.map(f).sequence == fa.traverse(f)

Traversable, an example

  • you maybe have a value. If you have a value, you want to use it to make an http call.
type Data

def makeHttpCall(d: Data): IO[ResponseData] = ???
def getData(): IO[Option[Data]] = ???

for {
  d <- getData
  result <- d match {
    case Some(data) => makeHttpCall(data)
	case _ => Option.empty[Data].pure[IO]
  }
} yield result
  • no!

Traversable, an example

type Data

def makeHttpCall(d: Data): IO[ResponseData] = ???
def getData(): IO[Option[Data]] = ???

for {
  d <- getData
  result <- d traverse { makeHttpCall _ }
} yield result
  • now future readers don't have to guess if you're doing anything weird in your None/Some cases!
  • but obviously if you need to do something weird you should match and do that

Variance

Functors again

  • gonna switch to some Haskell syntax to match some examples
newtype T1 a = T1 (Int -> a)

newtype T2 a = T2 (a -> Int)

newtype T3 a = T3 (a -> a)

newtype T4 a = T4 ((Int -> a) -> Int)

newtype T5 a = T5 ((a -> Int) -> Int)
  • which of these have functors?
  • or: for which of these, if I give you an f: a => b, can you give me a Tx b?

Why don't some of them work?

  • positive and negative position
  • a variable appears in positive position when it is on the right-hand side of an arrow, or as a type parameter to a coproduct or product
  • a variable appears in negative position when it is on the left-hand side of an arrow
  • these multiply the way you'd expect

The examples again

-- a in positive only
newtype T1 a = T1 (Int -> a)

-- a in negative only
newtype T2 a = T2 (a -> Int)

-- a in _both_ positions
newtype T3 a = T3 (a -> a)

-- a in positive position in (Int -> a), but (Int -> a) in negative position
newtype T4 a = T4 ((Int -> a) -> Int)

-- a in negative position in (a -> Int), but (a -> Int) in negative position
newtype T5 a = T5 ((a -> Int) -> Int)

Other typeclasses

  • when a is in strictly negative position, we have a Contravariant
  • Contravariant provides contramap
  • when a appears in both positive and negative position, we have an Invariant
  • Invariant provides imap

A more concrete example

  • circe provides us a Decoder typeclass
// simplified
trait Decoder[T] {
  def decode(js: Json): Either[Error, T]
}
  • what position is T in?
  • and so:
val decTimeRange: Decoder[(Option[Instant], Option[Instant])] = Decoder[String] map { str =>
  ???
}

A more concrete example

  • circe provides us an Encoder typeclass
// simplified
trait Encoder[T] {
  def encode(thing: T): Json
}
  • what position in T in?
  • and so:
// Encoder.encodeString: Encoder[String]
// _.toString: Instant => String
implicit val encodeInstant: Encoder[Instant] =
  Encoder.encodeString.contramap[Instant](_.toString)

A more concrete example

  • Skunk provides us a Codec ... not actually a typeclass, but work with me here
// simplified
trait Codec[T] {
  def toProduct(thing: T): Product
  def fromProduct(p: Product): T
}
  • what position is T in?
  • and so:
val codec: Codec[Brand] =
  (uuid.cimap[BrandId] ~ varchar.cimap[BrandName]).imap {
    case i ~ n => Brand(i, n)
  }(b => b.uuid ~ b.name)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.