title | author | patat | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
Typeclasses and Variance |
James Santucci |
|
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 aSemigroup
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
, notF[_]
T
is a type, whileF[_]
is a type constructor- typeclasses of
T
will define things we can do with values. typeclasses ofF[_]
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
- Some contexts: eventuality (
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 ofF[_]
, provided the function arguments align with the wrapped types inF
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 ofF[_]
andF
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 aTx 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 aContravariant
Contravariant
providescontramap
- when
a
appears in both positive and negative position, we have anInvariant
Invariant
providesimap
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)