590

# Comonads and the game of life

Slides from my NE Scala talk.

March 13, 2020

## Transcript

2. ### WHO AM I? ▸ Senior Software Engineer at 47 Degrees

▸ Humble functional programmer ▸ Scala by way of Poetics
3. ### 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.

5. ### METHODOLOGY METAPHOR ▸ Building a mental model through connections ▸

Fun but perilous ▸ Monads are burritos ▸ Functors are boxes
6. ### METHODOLOGY FIRST PRINCIPLES ▸ The essential building blocks ▸ Understand

how Comonads ﬁll a niche ▸ Scala encoding
7. ### 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

10. ### MONADS ALLOW US TO… ▸ Lift values into a context

▸ Chain computations which happen in a context ▸ Stop immediately on failures
11. ### 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
12. ### COMONADS ALLOW US TO… ▸ Extract a value from its

context ▸ Chain together functions which rely on global context to produce a local value

22. ### 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]
23. ### 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

25. ### 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]]
26. ### 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

29. ### ZIPPERS ▸ Represent data along with a current focus or

cursor ▸ Allow for easy relative movement ▸ Allow for efﬁcient local edits of data structure
30. ### case class Zipper[A](  left: Stream[A],  focus: A,  right: Stream[A] )

STREAM ZIPPER
31. ### 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

35. ### COMONAD INSTANCE FOR ZIPPERS ▸ The focus allows us to

call extract ▸ Coﬂatten creates a Zipper of all possible Zippers ▸ Every element is in focus once
36. ### implicit def ZipperComonad: Comonad[Zipper] = {  new Comonad[Zipper] {

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

??? } COMONAD INSTANCE FOR ZIPPERS

40. ### new Comonad[Zipper] {    ???     override def coflatten[A](w:

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

w, w.duplicateRights) COMONAD INSTANCE FOR ZIPPERS NEW FOCUS
42. ### 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]
43. ### def duplicateLefts[B]: Stream[Zipper[A]] =  unfold(this)((z: Zipper[A]) =>   z.maybeLeft.map((x: Zipper[A])

=> (x, x))  ) COMONAD INSTANCE FOR ZIPPERS
44. ### 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…
45. ### 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

53. ### CONWAY’S GAME OF LIFE THE SETUP ▸ Played on 2D

grid ▸ A player seeds the initial grid ▸ Evolution of successive generations
54. ### 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

59. ### 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
60. ### 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
61. ### 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
62. ### 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
63. ### def getNeighbors: List[A] =  List(  this.north.extract,  this.east.extract,  […]  this.south.east.extract,  this.south.west.extract

) WALK THE NEIGHBORHOOD

65. ### 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

67. ### new Comonad[GridZipper] { override def extract[A](w: GridZipper[A]): A =  w.value.focus.focus

??? } A COMONAD FOR GRIDZIPPERS
68. ### new Comonad[GridZipper] { ??? override def coflatten[A](w: GridZipper[A]): GridZipper[GridZipper[A]] =

map(GridZipper(nest(nest(w.value))))(GridZipper(_)) } A COMONAD FOR GRIDZIPPERS
69. None
70. None

72. ### 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
73. ### 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
74. ### 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
75. ### 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
76. ### 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
77. ### 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
78. ### 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
79. ### 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
80. ### 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!
81. ### 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 …

84. ### 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