title | author | patat | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
Typeclasses and Variance |
James Santucci |
|
- Things with the shape
F[_]
- That are used as:
TYPE -> CONSTRAINT
CONSTRAINT
constructors, without inheritance
- 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
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
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
- 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 (
- 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
- 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)
- 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
- 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
- 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)
- 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!
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
- 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
?
- 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
-- 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)
- 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
- 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 =>
???
}
- 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)
- 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)