Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Comonads and the game of life

Rebecca
March 13, 2020

Comonads and the game of life

Slides from my NE Scala talk.

Rebecca

March 13, 2020
Tweet

Other Decks in Technology

Transcript

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

    ▸ Humble functional programmer ▸ Scala by way of Poetics
  2. 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.
  3. METHODOLOGY METAPHOR ▸ Building a mental model through connections ▸

    Fun but perilous ▸ Monads are burritos ▸ Functors are boxes
  4. 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
  5. MONADS ALLOW US TO… ▸ Lift values into a context

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

    context ▸ Chain together functions which rely on global context to produce a local value
  8. 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
  9. 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
  10. "

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

    cursor ▸ Allow for easy relative movement ▸ Allow for efficient local edits of data structure
  12. 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
  13. 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
  14. new Comonad[Zipper] {
 
 ??? 
 
 override def coflatten[A](w:

    Zipper[A]): Zipper[Zipper[A]] = 
 Zipper(w.duplicateLefts, w, w.duplicateRights) } COMONAD INSTANCE FOR ZIPPERS
  15. 
 
 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]
  16. 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…
  17. 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
  18. CONWAY’S GAME OF LIFE THE SETUP ▸ Played on 2D

    grid ▸ A player seeds the initial grid ▸ Evolution of successive generations
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. new Comonad[GridZipper] { ??? override def coflatten[A](w: GridZipper[A]): GridZipper[GridZipper[A]] =


    map(GridZipper(nest(nest(w.value))))(GridZipper(_)) } A COMONAD FOR GRIDZIPPERS
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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!
  35. 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 …
  36. 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