Slide 1

Slide 1 text

COMONADS
 AND THE GAME OF LIFE REBECCA MARK

Slide 2

Slide 2 text

WHO AM I? ▸ Senior Software Engineer at 47 Degrees ▸ Humble functional programmer ▸ Scala by way of Poetics

Slide 3

Slide 3 text

POETICS COPOETICS Degree Degree Financial 
 Security Financial
 Security (AKA PROGRAMMING) ✴ ✴ “You cannot get the news from poems yet men die every day for lack of what is found there” - W.C.W.

Slide 4

Slide 4 text

METHODOLOGY

Slide 5

Slide 5 text

METHODOLOGY METAPHOR ▸ Building a mental model through connections ▸ Fun but perilous ▸ Monads are burritos ▸ Functors are boxes

Slide 6

Slide 6 text

METHODOLOGY FIRST PRINCIPLES ▸ The essential building blocks ▸ Understand how Comonads fill a niche ▸ Scala encoding

Slide 7

Slide 7 text

METHODOLOGY PRAXIS ▸ Theory motived by doing ▸ Look at a data type with a comonad instance ▸ Understand why it might be useful ▸ Application of comonads in Conway’s game of life

Slide 8

Slide 8 text

THIS IS NOT A MONAD TUTORIAL

Slide 9

Slide 9 text

BUT…

Slide 10

Slide 10 text

MONADS ALLOW US TO… ▸ Lift values into a context ▸ Chain computations which happen in a context ▸ Stop immediately on failures

Slide 11

Slide 11 text

trait Monad[F[_]] extends Functor[F]{ def pure[A](a: A): F[A] def flatten[A](x: F[F[A]]): F[A] def flatMap[A,B](x: F[A])(f: A => F[B]): F[B] } MONAD

Slide 12

Slide 12 text

COMONADS ALLOW US TO… ▸ Extract a value from its context ▸ Chain together functions which rely on global context to produce a local value

Slide 13

Slide 13 text

CONTEXT DEPENDENT COMPUTATIONS

Slide 14

Slide 14 text

def pure[A](a: A): F[A] MONAD

Slide 15

Slide 15 text

def extract[A](a: F[A]): A COMONAD

Slide 16

Slide 16 text

def extract[A](a: F[A]): A COMONAD def pure[A](a: A): F[A]

Slide 17

Slide 17 text

def flatten[A](x: F[F[A]]): F[A] MONAD

Slide 18

Slide 18 text

def coflatten[A](x: F[A]]): F[F[A]] COMONAD

Slide 19

Slide 19 text

def coflatten[A](x: F[A]]): F[F[A]] COMONAD def flatten[A](x: F[F[A]]): F[A]

Slide 20

Slide 20 text

def flatMap[A,B](x: F[A])(f: A => F[B]): F[B] MONAD

Slide 21

Slide 21 text

def coflatMap[A,B](x: F[A])(f: F[A] => B): F[B] COMONAD

Slide 22

Slide 22 text

def coflatMap[A,B](x: F[A])(f: F[A] => B): F[B] COMONAD def flatMap[A,B](x: F[A])(f: A => F[B]): F[B]

Slide 23

Slide 23 text

trait Comonad[F[_]] extends Functor[F]{ def extract[A](x: F[A]): A def coflatten[A](x: F[A]]): F[F[A]] def coflatMap[A,B](x: F[A])(f: F[A] => B): F[B] } COMONAD

Slide 24

Slide 24 text

def coflatMap[A,B](x: F[A])(f: F[A] => B): F[B] = map(coflatten(x))(f) COMONAD

Slide 25

Slide 25 text

def coflatMap[A,B](x: F[A])(f: F[A] => B): F[B] = map(coflatten(x))(f) COMONAD DUPLICATES THE DATA STRUCTURE F[F[A]]

Slide 26

Slide 26 text

def coflatMap[A,B](x: F[A])(f: F[A] => B): F[B] = map(coflatten(x))(f) COMONAD F[_] HAS A FUNCTOR SO WE CAN MAP OVER OUR DUPLICATED STRUCTURE AND APPLY f

Slide 27

Slide 27 text

"

Slide 28

Slide 28 text

" ZIPPERS

Slide 29

Slide 29 text

ZIPPERS ▸ Represent data along with a current focus or cursor ▸ Allow for easy relative movement ▸ Allow for efficient local edits of data structure

Slide 30

Slide 30 text

case class Zipper[A](
 left: Stream[A],
 focus: A,
 right: Stream[A] ) STREAM ZIPPER

Slide 31

Slide 31 text

def moveRight: Zipper[A] = 
 if (right.isEmpty) this
 else Zipper(focus #:: left, right.head, right.tail)
 def moveLeft: Zipper[A] = 
 if (left.isEmpty) this
 else Zipper(left.tail, left.head, focus #:: right) STREAM ZIPPER

Slide 32

Slide 32 text

1 2 3 … … MOVE RIGHT

Slide 33

Slide 33 text

1 2 3 … … MOVE RIGHT

Slide 34

Slide 34 text

ZIPPERS ARE COMONADS!

Slide 35

Slide 35 text

COMONAD INSTANCE FOR ZIPPERS ▸ The focus allows us to call extract ▸ Coflatten creates a Zipper of all possible Zippers ▸ Every element is in focus once

Slide 36

Slide 36 text

implicit def ZipperComonad: Comonad[Zipper] = {
 new Comonad[Zipper] {
 
 ??? 
 
 }
 } COMONAD INSTANCE FOR ZIPPERS

Slide 37

Slide 37 text

new Comonad[Zipper] {
 override def extract[A](w: Zipper[A]): A = w.focus
 
 ??? } COMONAD INSTANCE FOR ZIPPERS

Slide 38

Slide 38 text

1 2 3 … … EXTRACT

Slide 39

Slide 39 text

1 2 3 … … EXTRACT

Slide 40

Slide 40 text

new Comonad[Zipper] {
 
 ??? 
 
 override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] = 
 Zipper(w.duplicateLefts, w, w.duplicateRights) } COMONAD INSTANCE FOR ZIPPERS

Slide 41

Slide 41 text


 
 override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] = 
 Zipper(w.duplicateLefts, w, w.duplicateRights) COMONAD INSTANCE FOR ZIPPERS NEW FOCUS

Slide 42

Slide 42 text


 
 override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] = 
 Zipper(w.duplicateLefts, w, w.duplicateRights) COMONAD INSTANCE FOR ZIPPERS BUILDS A STREAM OF ZIPPER[A]

Slide 43

Slide 43 text

def duplicateLefts[B]: Stream[Zipper[A]] =
 unfold(this)((z: Zipper[A]) => 
 z.maybeLeft.map((x: Zipper[A]) => (x, x))
 ) COMONAD INSTANCE FOR ZIPPERS

Slide 44

Slide 44 text

def duplicateLefts[B]: Stream[Zipper[A]] =
 unfold(this)((z: Zipper[A]) => 
 z.maybeLeft.map((x: Zipper[A]) => (x, x))
 ) COMONAD INSTANCE FOR ZIPPERS FROM THIS ZIPPER, 
 WE’RE BUILDING A STREAM…

Slide 45

Slide 45 text

def duplicateLefts[B]: Stream[Zipper[A]] =
 unfold(this)((z: Zipper[A]) => 
 z.maybeLeft.map((x: Zipper[A]) => (x, x))
 ) COMONAD INSTANCE FOR ZIPPERS KEEP REFOCUSING LEFT, 
 UNTIL NO MORE VALUES

Slide 46

Slide 46 text

COFLATTEN

Slide 47

Slide 47 text

COFLATTEN LEFTS RIGHTS FOCUS

Slide 48

Slide 48 text

COFLATTEN LEFTS RIGHTS FOCUS

Slide 49

Slide 49 text

SO WHAT?

Slide 50

Slide 50 text

John Conway

Slide 51

Slide 51 text

CONWAY’S GAME OF LIFE

Slide 52

Slide 52 text

LIFE

Slide 53

Slide 53 text

CONWAY’S GAME OF LIFE THE SETUP ▸ Played on 2D grid ▸ A player seeds the initial grid ▸ Evolution of successive generations

Slide 54

Slide 54 text

CONWAY’S GAME OF LIFE THE RULES ▸ A live cell with two or three neighbors stays alive ▸ A dead cell with three live neighbors becomes a live cell ▸ All other live cells die in the next generation

Slide 55

Slide 55 text

ZIPPER = 1 DIMENSION

Slide 56

Slide 56 text

ZIPPER[ZIPPER[_]] = 2 DIMENSIONS

Slide 57

Slide 57 text

case class GridZipper[A](
 value: Zipper[Zipper[A]]
 ) REPRESENTING A GRID

Slide 58

Slide 58 text

A CELL’S STATE 
 DEPENDS ON ITS 
 NEIGHBORHOOD

Slide 59

Slide 59 text

def north: GridZipper[A] =
 GridZipper(value.moveLeft) def south: GridZipper[A] =
 GridZipper(value.moveRight)
 
 def east: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveRight)) def west: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveLeft)) WALKING A NEIGHBORHOOD

Slide 60

Slide 60 text

def north: GridZipper[A] =
 GridZipper(value.moveLeft) def south: GridZipper[A] =
 GridZipper(value.moveRight)
 
 def east: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveRight)) def west: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveLeft)) WALKING A NEIGHBORHOOD

Slide 61

Slide 61 text

def north: GridZipper[A] =
 GridZipper(value.moveLeft) def south: GridZipper[A] =
 GridZipper(value.moveRight)
 
 def east: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveRight)) def west: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveLeft)) WALKING A NEIGHBORHOOD

Slide 62

Slide 62 text

def north: GridZipper[A] =
 GridZipper(value.moveLeft) def south: GridZipper[A] =
 GridZipper(value.moveRight)
 
 def east: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveRight)) def west: GridZipper[A] =
 GridZipper(value.map(xAxis => xAxis.moveLeft)) WALKING A NEIGHBORHOOD

Slide 63

Slide 63 text

def getNeighbors: List[A] =
 List(
 this.north.extract,
 this.east.extract,
 […]
 this.south.east.extract,
 this.south.west.extract
 ) WALK THE NEIGHBORHOOD

Slide 64

Slide 64 text

NEIGHBORHOOD LIFE CYCLE

Slide 65

Slide 65 text

LIFECYCLE WE NEED A FUNCTION THAT… ▸ Takes in the current grid ▸ Allows us to get the neighborhood of a focus ▸ Apply the rules for the game ▸ Returns a grid

Slide 66

Slide 66 text

WE NEED A COMONAD!

Slide 67

Slide 67 text

new Comonad[GridZipper] { override def extract[A](w: GridZipper[A]): A =
 w.value.focus.focus ??? } A COMONAD FOR GRIDZIPPERS

Slide 68

Slide 68 text

new Comonad[GridZipper] { ??? override def coflatten[A](w: GridZipper[A]): GridZipper[GridZipper[A]] =
 map(GridZipper(nest(nest(w.value))))(GridZipper(_)) } A COMONAD FOR GRIDZIPPERS

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

NEIGHBORHOOD LIFE CYCLE

Slide 72

Slide 72 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES DEAD CELL = 0 ALIVE CELL = 1

Slide 73

Slide 73 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES

Slide 74

Slide 74 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES

Slide 75

Slide 75 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES

Slide 76

Slide 76 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES CELL IS ALIVE, 2 OR 3 NEIGHBORS STAY ALIVE

Slide 77

Slide 77 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES 3 ALIVE NEIGHBORS CELL BECOMES ALIVE

Slide 78

Slide 78 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES ALL OTHER CASES, AN ALIVE CELL DIES

Slide 79

Slide 79 text

def cellLifecycle(grid: GridZipper[Int]): Int = {
 val neighborList: List[Int] = grid.getNeighbors
 (neighborList.sum, grid.extract) match {
 case (sum, 1) if sum == 2 || sum == 3 => 1
 case (3, 0) => 1
 case (_, 1) => 0
 case (_, x) => x
 }
 } MODEL THE RULES DEAD CELLS STAY DEAD

Slide 80

Slide 80 text

def generation(grid: GridZipper[Int]): GridZipper[Int] = {
 grid.coflatMap(cellLifecycle)
 } MODEL THE GENERATION TAKES OUR CELL LIFECYCLE FUNCTION AND EXTENDS IT OVER THE ENTIRE GRID!

Slide 81

Slide 81 text

THINGS WE KNOW ▸ Comonads for context dependent computations ▸ Monads as context producing ▸ Zippers are one example of a comonadic data structure ✴ Which is to say …

Slide 82

Slide 82 text

PICK THE RIGHT ABSTRACTION

Slide 83

Slide 83 text

DEMO TIME!


Slide 84

Slide 84 text

POSTSCRIPT ▸ Link to source code: https://github.com/rlmark/ comonadic_life ▸ Link to blog post about this code: https://www. 47deg.com/blog/game-of-life-scala/ ▸ Stuck in the middle with you… https:// personal.cis.strath.ac.uk/conor.mcbride/Dissect.pdf ▸ OG Zippers: https://www.st.cs.uni-saarland.de/edu/ seminare/2005/advanced-fp/docs/huet-zipper.pdf