Eli Jordan
@eliaskjordan
http://eli-jordan.github.io
I am... Lead Developer at Tapad
I am... a Scala Developer
I am not... a Theorist
I am not... an expert in Category Theory
I am... a Functional Programmer
What is a Comonad?
Zipper as a Metaphor for Comonads
Conway's Game of Life
trait Monad[F[_]] {
def unit[A](a: A): F[A]
def join[A](ffa: F[F[A]]): F[A]
}
trait Comonad[F[_]] {
def counit[A](fa: F[A]): A
def cojoin[A](fa: F[A]): F[F[A]]
}
unit : A => F[A]
counit: F[A] => A
join : F[F[A]] => F[A]
cojoin: F[A] => F[F[A]]
Takes an apple and puts it in a box
Takes an apple in a box, and extracts the apple.
Takes an apple in a box, in a box, and throws out one of the boxes.
Takes an apple in a box, and puts it in another box.
In a Monad the unit function takes a pure value, and wraps it in an F structure F[A]
In a Comonad the counit function takes an F structure and extracts a pure value A
In a Monad the join function takes two layers of F structure F[F[A]] and collapses it into one layer F[A]
In a Comonad the cojoin function takes one layer of F structure F[A] and duplicates it F[F[A]]
case class StreamZipper[A](
left: Stream[A], focus: A, right: Stream[A]) {
def moveLeft: StreamZipper[A] =
new StreamZipper[A](
left.tail, left.head, focus #:: right)
def moveRight: StreamZipper[A] =
new StreamZipper[A](
focus #:: left, right.head, right.tail)
}
implicit object ZipperComonad extends
Comonad[StreamZipper] {
def counit[A](fa: StreamZipper[A]): A =
fa.focus
// ...
}
case class StreamZipper[A](
left: Stream[A], focus: A, right: Stream[A]) {
// ...
// A stream of zippers, with the focus set to each
// element on the left
private lazy val lefts: Stream[StreamZipper[A]] =
Stream.iterate(this)(_.moveLeft)
.tail.zip(left).map(_._1)
// A stream of zippers, with the focus set to each
// element on the right
private lazy val rights: Stream[StreamZipper[A]] =
Stream.iterate(this)(_.moveRight)
.tail.zip(right).map(_._1)
lazy val cojoin: StreamZipper[StreamZipper[A]] =
new StreamZipper[StreamZipper[A]](lefts, this, rights)
}
implicit object ZipperComonad extends
Comonad[StreamZipper] {
def counit[A](fa: StreamZipper[A]): A =
fa.focus
def cojoin[A](fa: StreamZipper[A]):
StreamZipper[StreamZipper[A]] = fa.cojoin
}
Now, the comonad instance is trivial
But, lets first look at our definitions again
trait Monad[F[_]] {
def unit[A](a: A): F[A]
def join[A](ffa: F[F[A]]): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B])
(implicit F: Functor[F]): F[B] = ???
}
trait Comonad[F[_]] {
def counit[A](fa: F[A]): A
def cojoin[A](fa: F[A]): F[F[A]]
def coflatMap[A, B](fa: F[A])(f: F[A] => B)
(implicit F: Functor[F]): F[B] = ???
}
trait Monad[F[_]] {
def unit[A](a: A): F[A]
def join[A](ffa: F[F[A]]): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B])(...): F[B] =
join(fa.map(f))
}
trait Comonad[F[_]] {
def counit[A](fa: F[A]): A
def cojoin[A](fa: F[A]): F[F[A]]
def coflatMap[A, B](fa: F[A])(f: F[A] => B)(...): F[B] =
cojoin(fa).map(f)
}
cojoin(fa).map(f)
cojoin(fa).map(f)
f: StreamZipper[Int] => Int
def avg(a: StreamZipper[Int]): Double = {
val left = a.moveLeft.focus
val current = a.focus
val right = a.moveRight.focus
(left + current + right) / 3d
}
// Note: The StreamZipper constructor reverses
// the first list.
StreamZipper(List(1, 2, 3), 4, List(5, 6, 7))
.coflatMap(avg).toList
// List(1.33.., 2.0, 3.0, 4.0, 5.0, 6.0, 6.66..)
case class Grid[A](
value: StreamZipper[StreamZipper[A]]) {
def moveUp: Grid[A] =
Grid(value.moveLeft)
def moveDown: Grid[A] =
Grid(value.moveRight)
def moveLeft: Grid[A] =
Grid(value.map(_.moveLeft))
def moveRight: Grid[A] =
Grid(value.map(_.moveRight))
def counit: A =
value.counit.counit
def cojoin: Grid[Grid[A]] = ??? // TODO
}
case class Grid[A](
value: StreamZipper[StreamZipper[A]]) {
def cojoin: Grid[Grid[A]] =
Grid(layer(layer(value))).map(Grid.apply)
// Notice that the implementation is very similar to
// what we had in our original zipper except that we
// need to make use of the 'map' function in the iteration.
private def layer[X](u: StreamZipper[StreamZipper[X]]):
StreamZipper[StreamZipper[StreamZipper[X]]] = {
val lefts = Stream.iterate(u)(ssx => ssx.map(_.moveLeft))
.tail.zip(u.left).map(_._1)
val rights = Stream.iterate(u)(ssx => ssx.map(_.moveRight))
.tail.zip(u.right).map(_._1)
StreamZipper(lefts, u, rights)
}
}
Using the same derivation of coflatMap we get
case class Grid[A](...) {
// ...
def coflatMap[B](f: Grid[A] => B): Grid[B]
}
The function f is what we need to implement.
It will define the rules of the game.
def conway(grid: Grid[Boolean]): Boolean = ???
def conway(grid: Grid[Boolean]): Boolean = {
val liveCount = neighbours(grid).count(identity)
grid.counit match {
// under population
case true if liveCount < 2 => false
// thriving
case true if liveCount == 2 || liveCount == 3 => true
// over population
case true if liveCount > 3 => false
// reproduction
case false if liveCount == 3 => true
}
}
Now, translating the rules is very simple...
def neighbours[A](grid: Grid[A]): List[A] =
List(
grid.moveUp,
grid.moveDown,
grid.moveLeft,
grid.moveRight,
grid.moveUp.moveLeft,
grid.moveUp.moveRight,
grid.moveDown.moveLeft,
grid.moveDown.moveRight
).map(_.counit)
For completeness
// Left identity
wa.cojoin.counit <-> wa
// Right identity
wa.coflatMap(_.counit) <-> wa
// Associativity
wa.cojoin.cojoin <-> wa.coflatMap(_.cojoin)