Pro Yearly is on sale from $80 to $50! »

Comonads and the game of life

7254674842034b8f6ce2bfcff4767917?s=47 Rebecca
March 13, 2020

Comonads and the game of life

Slides from my NE Scala talk.

7254674842034b8f6ce2bfcff4767917?s=128

Rebecca

March 13, 2020
Tweet

Transcript

  1. COMONADS
 AND THE GAME OF LIFE REBECCA MARK

  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.
  4. METHODOLOGY

  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 fill 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
  8. THIS IS NOT A MONAD TUTORIAL

  9. BUT…

  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
  13. CONTEXT DEPENDENT COMPUTATIONS

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

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

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

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

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

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

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

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

  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
  24. def coflatMap[A,B](x: F[A])(f: F[A] => B): F[B] = map(coflatten(x))(f) 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
  27. "

  28. " ZIPPERS

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

    cursor ▸ Allow for easy relative movement ▸ Allow for efficient 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
  32. 1 2 3 … … MOVE RIGHT

  33. 1 2 3 … … MOVE RIGHT

  34. ZIPPERS ARE COMONADS!

  35. 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
  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
  38. 1 2 3 … … EXTRACT

  39. 1 2 3 … … EXTRACT

  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
  46. COFLATTEN

  47. COFLATTEN LEFTS RIGHTS FOCUS

  48. COFLATTEN LEFTS RIGHTS FOCUS

  49. SO WHAT?

  50. John Conway

  51. CONWAY’S GAME OF LIFE

  52. LIFE

  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
  55. ZIPPER = 1 DIMENSION

  56. ZIPPER[ZIPPER[_]] = 2 DIMENSIONS

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

  58. A CELL’S STATE 
 DEPENDS ON ITS 
 NEIGHBORHOOD

  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
  64. NEIGHBORHOOD LIFE CYCLE

  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
  66. WE NEED A COMONAD!

  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
  71. NEIGHBORHOOD LIFE CYCLE

  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 …
  82. PICK THE RIGHT ABSTRACTION

  83. DEMO TIME!


  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