Greg Pfeil
Formation.ai – ML for customer relationships
- Ross Baker (@rossabaker)
- Kris Nuttycombe (@nuttycom)
- Paul Snively (@paul_snively)
- andyscott/droste – Andy Scott (@andygscott)
- vil1/recursion-schemes-cookbook – Valentin Kasas (@ValentinKasas)
I’m also working on a recursion scheme cookbook with one of the organizers(?) (member of the program committee?) here – Valentin Kasas.
But this talk isn’t about recursion schemes. At least not directly. But in a different way, it is, because it’s about everything, because it’s about … Categories!
“Most if not all constructions in category theory are parametric in the underlying category, resulting in a remarkable economy of expression. […] This possibly leads to a new style of programming, which could be loosely dubbed as category-parametric programming.”
I’m just checking which of these things you may be more-or-less familiar with. So, keep your hands up for any slides you understand at a glance, and put them down for slides you don’t understand. This helps me figure out which other slides I should maybe move through quickly or explain more carefully. If you do find yourself bewildered by the end – that’s my fault, not yours. I will happily sit down with anyone to work through any and all of it. (And you can also find me on the Internet if you think of something after the conference has ended – my Twitter and email are on the bottom of every slide).There is nothing about not knowing any of these that makes you a “worse” programmer than someone who does know them – different people are introduced to different ideas at different times. So, don’t feel like you should be familiar with these – just
sbt.version=1.2.3
inThisBuild(Seq(
scalaOrganization := "org.typelevel",
scalaVersion := "2.12.4-bin-typelevel-4",
scalacOptions := Seq(
"-language:higherKinds",
"-Ykind-polymorphism"),
libraryDependencies := Seq(
"org.typelevel" %% "cats-core" % "1.3.1",
"org.typelevel" %% "cats-effect" % "1.0.0")))
addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.8")
import cats.implicits._
package object CategoryParametric {
package object Calibration {
Either[String, ?]
cats.arrow.FunctionK[List, ?[_]]
def f: Boolean => Char
def g: String => Boolean
f <<< g
(a: String) => f(g(a))
def map[A, B](fa: List[A])(f: A => B): List[B]
def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B]
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
trait Monad[M[_]] extends cats.Applicative[M] {
def flatMap[A, B](fa: M[A])(f: A => M[B]): M[B]
}
trait Monoid[A] {
def empty: A
def combine(a: A, b: A): A
}
trait Category[⟹[_, _]] {
def id[A]: A ⟹ A
def compose[A, B, C](f: B ⟹ C, g: A ⟹ B): A ⟹ C
}
}
trait Category[⟹[_, _]] {
def id[A]: A ⟹ A
def compose[A, B, C](f: B ⟹ C, g: A ⟹ B): A ⟹ C
}
- objects
- morphisms between objects
- that can be composed
- there is an identity morphism for each object
implicit val scal: Category[Function1] = new Category[Function1] {
def id[A] = Predef.identity
def compose[A, B, C](f: B => C, g: A => B) = f.compose(g)
}
// from cats.data
final case class Kleisli[F[_], A, B](run: A => F[B])
implicit def kleisli[M[_]](implicit M: cats.Monad[M])
: Category[Kleisli[M, ?, ?]] =
new Category[Kleisli[M, ?, ?]] {
def id[A] = Kleisli(M.pure[A])
def compose[A, B, C](f: Kleisli[M, B, C], g: Kleisli[M, A, B]) =
Kleisli(a => M.flatMap(g.run(a))(f.run))
}
Category
, is there anything that’s missing?
Proper values!
The only values we have are the morphisms. And the only thing we can do to them is compose them.
Scala is not great at composition. It expects things to be applied, otherwise you have to provide it with lots of types to tell it what you want.
trait Category[⟹[_, _]] {
def id[A]: A ⟹ A
def compose[A, B, C](f: B ⟹ C, g: A ⟹ B): A ⟹ C
}
The same reason we might care about interfaces, or type classes – abstraction!
Even if we can’t have a fully abstract implementation, understanding the common abstraction can help us see larger similarities between things.
Category theory is the ultimate abstraction. Everything from every field of mathematics (like, type theory) maps to category theory. Not only can you see how ideas in your particular area relate to each other more clearly, but you can also see how your ideas map to ideas in other branches of mathematics.
- Eugenia Cheng – Category Theory and Life
\usetikzlibrary{cd}
\begin{tikzcd}
A \ar[r, "f"] \ar[d, "F"] & B \ar[d, "F"] \\
A_F \ar[r, "f_F"] & B_F
\end{tikzcd}
\usetikzlibrary{cd}
\begin{tikzcd}
A \ar[r, "f"] \ar[d, "F"] & B \ar[d, "F"] \\
A_F \ar[r, "map(f)"] & B_F
\end{tikzcd}
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
trait Functorʹ[F[_]] {
def map[A, B](f: A => B)(fa: F[A]): F[B]
}
trait Functorʹʹ[F[_]] {
def map[A, B](f: A => B): F[A] => F[B]
}
trait Endofunctorʹ[⟹[_, _], F[_]] {
def map[A, B](f: A ⟹ B): F[A] ⟹ F[B]
}
trait Exofunctor[⟹[_, _], ⟾[_, _], F[_]] {
def map[A, B](f: A ⟹ B): F[A] ⟾ F[B]
}
type Endofunctor[⟹[_, _], F[_]] = Exofunctor[⟹, ⟹, F]
type Functorʹʹʹ[F[_]] = Endofunctor[Function1, F]
// `map[A, B](f: A => B): F[A] => F[B]` is `map`
type Traverse[M[_], F[_]] = Endofunctor[Kleisli[M, ?, ?], F]
// `map[A, B](f: A => M[B]): F[A] => M[F[B]]` is `traverse`
implicit def optionTraverse[M[_]: Applicative] =
new Traverse[M, Option] {
def map[A, B](f: Kleisli[M, A, B]) = null
}
implicit def idTraverse[M[_]] = new Traverse[M, cats.Id] {
def map[A, B](f: Kleisli[M, A, B]) = null
}
implicit val ioTraverse = new Traverse[cats.Id, cats.effect.IO] {
def map[A, B](f: Kleisli[cats.Id, A, B]) = null
}
type Functorʹʹʹʹ[F[_]] = Traverse[cats.Id, F]
// `map[A, B](f: A => Id[B]): F[A] => Id[F[B]]` is `map`
type KleisliFunctor[M[_], F[_]] =
Exofunctor[cats.data.Kleisli[M, ?, ?], Function1, F]
type FunctorFilter[F[_]] = KleisliFunctor[Option, F]
// `map[A, B](f: A => Option[B]): F[A] => F[B]` is `mapFilter`
type FlatMap[F[_]] = KleisliFunctor[F, F]
// `map[A, B](f: A => F[B]): F[A] => F[B]` is `flatMap`
type CokleisliFunctor[M[_], F[_]] =
Exofunctor[cats.data.Cokleisli[M, ?, ?], Function1, F]
type CoflatMap[F[_]] = CokleisliFunctor[F, F]
// `map[A, B](f: F[A] => B): F[A] => F[B]` is `coflatMap`
// from cats.data
final case class Op[⟹[_, _], A, B](run: B ⟹ A)
type Presheaf[⟹[_, _], F[_]] = Exofunctor[Op[⟹, ?, ?], ⟹, F]
type Contravariant[F[_]] = Presheaf[Function1, F]
// `map[A, B](f: B => A): F[A] => F[B]` is `contramap`
// from cats.data
final case class Op[⟹[_, _], A, B](run: B ⟹ A)
Op[Kleisli[F, ?, ?], A, B] // (A => F[B]) => (B => F[A])
Op[Function1, A, F[B]] // (A => F[B]) => (F[B] => A)
A ⇒ M[B]
?
If you’re in Scal, the category of Scala types, the dual would be M[B] ⇒ A
.
But if you’re in a Kleisli category, then the dual would be B ⇒ M[A]
.
I.e., in a Kleisli category, the M
is part of the morphism, in Scal it’s part of the object.
final case class OrdFunction1
[A: cats.kernel.Order, B: cats.kernel.Order]
(run: A => B)
val setFunctor = new Exofunctor[OrdFunction1, Function1, Set] {
def map[A, B](f: OrdFunction1[A, B]) = _.map(f.run)
}
val boolSet: Set[Boolean] =
setFunctor.map(
OrdFunction1[Int, Boolean](_ % 2 == 0))(
Set(0, 1, 2, 3))
trait Exofunctorʹ[⟹[_ , _ ], ⟾[_ , _ ], F[_ ]] {
def map[A , B ](f: A ⟹ B): F[A ] ⟾ F[B ]
}
trait ExofunctorK[⟹[_[_], _[_]], ⟾[_[_], _[_]], F[_[_], _]] {
def map[A[_], B[_]](f: A ⟹ B): F[A, ?] ⟾ F[B, ?]
}
type Hoist[F[_[_], _]] =
ExofunctorK[cats.arrow.FunctionK, cats.arrow.FunctionK, F]
trait Bifunctor[⟶[_, _], ⟹[_, _], ⟾[_, _], F[_, _]] {
def map[A, B, C, D](f: A ⟶ C, g: B ⟹ D): F[A, B] ⟾ F[C, D]
}
type Bifunctorʹ[F[_, _]] =
Bifunctor[Function1, Function1, Function1, F]
// `map[A, B, C, D](f: A => C, g: B => D): F[A, B] => F[C, D]`
// is `bimap`
type Profunctor[⟶[_, _], ⟹[_, _], F[_, _]] =
Bifunctor[cats.data.Op[⟹, ?, ?], ⟶, Function1, F]
type Profunctorʹ[F[_, _]] = Profunctor[Function1, Function1, F]
// `map[A, B, C, D](f: C => A, g: B => D): F[A, B] => F[C, D]`
// is `dimap`
type HomFunctor[⟹[_, _], F[_,_]] =
Bifunctor[cats.data.Op[⟹, ?, ?], ⟹, ⟹, F]
type Profunctorʹʹ[F[_, _]] = HomFunctor[Function1, F]
- breaks inference
- often wrapping and unwrapping
- can make type class inheritance difficult
- gives us (or at least me) a taste of something I want more of
trait Monoid[A] {
def empty: A
def combine(x: A, y: A): A
}
case class MonoidLaws[A](monoid: Monoid[A]) {
def associative(x: A, y: A, z: A) =
monoid.combine(monoid.combine(x, y), z) ==
monoid.combine(x, monoid.combine(y, z))
def leftIdentity(x : A) = monoid.combine(monoid.empty, x) == x
def rightIdentity(x : A) = monoid.combine(x, monoid.empty) == x
}
identity
isn’t a morphism, though. And is op
? How can we fix these?
trait Monoidʹʹ[⟹[_, _], A] {
def identity: Unit ⟹ A
def op: (A, A) ⟹ A
}
trait CMonoid[⟹[_, _], I, ⊗[_, _], A] {
def identity: I ⟹ A
def op: (A ⊗ A) ⟹ A
}
type Monoidʹ[A] = CMonoid[Function1, Unit, Tuple2, A]
Monoidʹ
looks a bit different than Cats’ version, right? We have to apply identity
to ()
, and we have to apply ap
to a single Tuple2
, rather than to a pair of arguments. We can always add another wrapper:
trait ProperMonoidʹ[A] extends CMonoid[Function1, Unit, Tuple2, A] {
def empty: A
def combine(a: A, b: A): A
final def identity = (_: Unit) => empty
final def op = (tup: (A, A)) => combine(tup._1, tup._2)
}
Monoid
we usually want, without losing the generality of CMonoid
.
Now we’re going to talk about a different kind of monoid …
trait CMonoidʹ[⟹[_ , _ ], I , ⊗[_ , _ ], A ] {
def identity: I ⟹ A
def op: (A ⊗ A) ⟹ A
}
trait CMonoidK[⟹[_[_], _[_]], I[_], ⊗[_[_], _[_], _], A[_]] {
def identity: I ⟹ A
def op: ⊗[A, A, ?] ⟹ A
}
trait Monad[M[_]]
extends CMonoidK[cats.arrow.FunctionK,
cats.Id,
cats.data.Nested,
M] {
def pure[A](a: A): M[A]
def join[A](fa: M[M[A]]): M[A]
final def identity = λ[cats.arrow.FunctionK[cats.Id, M]](pure(_))
final def op =
λ[cats.arrow.FunctionK[cats.data.Nested[M, M, ?], M]](
a => join(a.value))
}
map2
and join
, rather than ap
and flatMap
. That’s a trivial issue to get around, though.
What’s more complicated is that we know that every Monad
implies an Applicative
, and we usually show that by having Monad[M[_]] extends Applicative[M]
, but we have a problem here – identity
matches up, but that would give us two distinct implementations of op
!
final abstract class Day[F[_], G[_], C] {
type A
type B
def fa: F[A]
def gb: G[B]
def f(a: A, b: B): C
}
trait Applicative[F[_]]
extends CMonoidK[cats.arrow.FunctionK, cats.Id, Day, F] {
def pure[A](a: A): F[A]
def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C]
final def identity = λ[cats.arrow.FunctionK[cats.Id, F]](pure(_))
final def op = λ[cats.arrow.FunctionK[Day[F, F, ?], F]](
day => map2(day.fa, day.gb)(day.f))
}
trait Monoidʹʹʹ[A] {
def empty: A
def combine(a: A, b: A): A
}
trait Categoryʹ[⟹[_, _]] {
def id[A]: A ⟹ A
def compose[A, B, C](f: B ⟹ C, g: A ⟹ B): A ⟹ C
}
trait MonoidB
[⟹[_[_, _], _[_, _]], I[_, _], ⊗[_[_, _], _[_, _], _, _],
A[_, _]] {
def identity: I ⟹ A
def op: ⊗[A, A, ?, ?] ⟹ A
}
trait FunctionB[F[_, _], G[_, _]] {
def apply[A, B](fab : F[A, B]): G[A, B]
}
final abstract class ComposeB[⟹[_, _], ⟾[_, _], A, B] {
type Z
def f: Z ⟹ B
def g: A ⟾ Z
}
trait Categoryʹʹ[⟹[_, _]]
extends MonoidB[FunctionB, cats.evidence.Is, ComposeB, ⟹] {
def id[A]: A ⟹ A
def compose[A, B, C](f: B ⟹ C, g: A ⟹ B): A ⟹ C
def identity = new FunctionB[cats.evidence.Is, ⟹] {
def apply[A, B](fab : cats.evidence.Is[A, B]) =
fab.substitute[A ⟹ ?](id)
}
// λ[FunctionB[ComposeB[⟹, ⟹, ?, ?], ⟹]](compose(fab.f, fab.g))
def op = new FunctionB[ComposeB[⟹, ⟹, ?, ?], ⟹] {
def apply[A, B](fab : ComposeB[⟹, ⟹, A, B]) =
compose(fab.f, fab.g)
}
}
But, what is a bifunctor? Different in category theory from Haskell / Scala. The B above is CT-ish, so, basically, any product category.
Requires Dotty or TypelevelThanks to Pascal & Miles.
trait CMonoid
[⟹[_, _],
I,
⊗[_, _],
A] {
def identity: I ⟹ A
def op: (A ⊗ A) ⟹ A
}
package object KindPoly {
trait Monoid
[⟹[_ <: AnyKind, _ <: AnyKind],
I <: AnyKind,
⊗ <: AnyKind, // ⊗[_ <: AnyKind, _ <: AnyKind] <: AnyKind,
A <: AnyKind] {
def identity: I ⟹ A
def op: ⊗ ⟹ A // def op: (A ⊗ A) ⟹ A
}
⊗
is a bifunctor, but that isn’t enforced by this definition.
We can still define it with bifunctors, but we need to explicitly mention A
twice in the ⊗
argument.
type ProperMonoid[A] = Monoid[Function1, Unit, (A, A), A]
⊗
argument.
type FakeMonoid[A] = Monoid[Function1, Unit, List, A]
Monoid
, but it means we have to be a bit careful with the definitions.
There’s another problem, in that the operations constrain I
and ⊗
to the same kind, and ⟹
must match the kinds of I
and A
in its two parameters, but there is nothing saying that the two parameters of ⟹
must be of the same kind. But since ⟹
is meant to be a morphism in a category, and all objects in a category must be valid on either side of a morphism, the kinds are required to align – but, again, it’s not enforced.
So, by generalizing Monoid
in this way, we’ve managed to unify many type classes
// Monoid
type ProperMonoid[A] = Monoid[Function1, Unit, (A, A), A]
// MonoidK
type MonoidK[F[_]] =
Monoid[cats.arrow.FunctionK, cats.Id, cats.data.Tuple2K[F, F, ?],
F]
type Applicative[F[_]] =
Monoid[cats.arrow.FunctionK, cats.Id, Day[F, F, ?], F]
type Monad[M[_]] =
Monoid[cats.arrow.FunctionK, cats.Id, cats.data.Nested[M, M, ?],
M]
// MonoidB
type TypeCategory[⟹[_, _]] =
Monoid[FunctionB, cats.evidence.Is, ComposeB[⟹, ⟹, ?, ?], ⟹]
AnyKind
on a type, and in the second case (using the same sort of trick from ⊗
), we have no way to apply the F
to A
or B
.
There may be some trick I’m unaware of to help with this.
trait Functor
[⟹[_ <: AnyKind, _ <: AnyKind],
⟾[_ <: AnyKind, _ <: AnyKind],
F[_ <: AnyKind] <: AnyKind] {
def map[A <: AnyKind, B <: AnyKind](f: A ⟹ B): F[A] ⟾ F[B]
}
trait Functor
[⟹[_ <: AnyKind, _ <: AnyKind],
⟾[_ <: AnyKind, _ <: AnyKind],
F <: AnyKind] {
def map[A <: AnyKind, B <: AnyKind](f: A ⟹ B): F ⟾ F
}
F[_ <: AnyKind] <: AnyKind
final case class Tuple2K[F[_], G[_], A](first: F[A], second: G[A])
final case class Tuple2K[F[_], G[_]][A](first: F[A], second: G[A])
}
trait Category[⟹[_ <: AnyKind, _ <: AnyKind]] {
def id[A <: AnyKind]: A ⟹ A
def compose[A <: AnyKind, B <: AnyKind, C <: AnyKind]
(g: B ⟹ C, f: A ⟹ B)
: A ⟹ C
}
}
Greg Pfeil
Formation.ai – ML for customer relationships
Thanks to
- Erik Osheim (@d6) for
kind-projector
, - Pascal Voitot (@mandubian) for
-Ykind-polymorphism
- Miles Sabin (@milessabin) for Typelevel Scala
- Rob Norris (@tpolecat)
- Typelevel.org in general
- Scala.io
- so many others inside and outside the Scala community