Instantly share code, notes, and snippets.

# jisantuc/typeclasses-and-variance.md Secret

Last active March 30, 2020 01:15
Show Gist options
• Save jisantuc/99a958e9eac67bd525c728fa39626077 to your computer and use it in GitHub Desktop.
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

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

• 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)```