$30 off During Our Annual Pro Sale. View Details »

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. COMONADS

    AND THE GAME OF LIFE
    REBECCA MARK

    View Slide

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

    View Slide

  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.

    View Slide

  4. METHODOLOGY

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  8. THIS IS NOT A
    MONAD TUTORIAL

    View Slide

  9. BUT…

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  13. CONTEXT
    DEPENDENT
    COMPUTATIONS

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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]

    View Slide

  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

    View Slide

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

    View Slide

  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]]

    View Slide

  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

    View Slide

  27. "

    View Slide

  28. "
    ZIPPERS

    View Slide

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

    View Slide

  30. case class Zipper[A](

    left: Stream[A],

    focus: A,

    right: Stream[A]
    )
    STREAM ZIPPER

    View Slide

  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

    View Slide

  32. 1 2 3 …

    MOVE RIGHT

    View Slide

  33. 1 2 3 …

    MOVE RIGHT

    View Slide

  34. ZIPPERS ARE COMONADS!

    View Slide

  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

    View Slide

  36. implicit def ZipperComonad: Comonad[Zipper] = {

    new Comonad[Zipper] {


    ??? 


    }

    }
    COMONAD INSTANCE FOR ZIPPERS

    View Slide

  37. new Comonad[Zipper] {

    override def extract[A](w: Zipper[A]): A = w.focus


    ???
    }
    COMONAD INSTANCE FOR ZIPPERS

    View Slide

  38. 1 2 3 …

    EXTRACT

    View Slide

  39. 1
    2
    3 …

    EXTRACT

    View Slide

  40. new Comonad[Zipper] {


    ??? 


    override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] = 

    Zipper(w.duplicateLefts, w, w.duplicateRights)
    }
    COMONAD INSTANCE FOR ZIPPERS

    View Slide



  41. override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] = 

    Zipper(w.duplicateLefts, w, w.duplicateRights)
    COMONAD INSTANCE FOR ZIPPERS
    NEW FOCUS

    View Slide



  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]

    View Slide

  43. def duplicateLefts[B]: Stream[Zipper[A]] =

    unfold(this)((z: Zipper[A]) => 

    z.maybeLeft.map((x: Zipper[A]) => (x, x))

    )
    COMONAD INSTANCE FOR ZIPPERS

    View Slide

  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…

    View Slide

  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

    View Slide

  46. COFLATTEN

    View Slide

  47. COFLATTEN
    LEFTS
    RIGHTS
    FOCUS

    View Slide

  48. COFLATTEN
    LEFTS
    RIGHTS
    FOCUS

    View Slide

  49. SO WHAT?

    View Slide

  50. John Conway

    View Slide

  51. CONWAY’S
    GAME OF
    LIFE







    View Slide

  52. LIFE








    View Slide

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

    View Slide

  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

    View Slide

  55. ZIPPER = 1 DIMENSION

    View Slide

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

    View Slide

  57. case class GridZipper[A](

    value: Zipper[Zipper[A]]

    )
    REPRESENTING A GRID

    View Slide

  58. A CELL’S STATE 

    DEPENDS ON ITS 

    NEIGHBORHOOD

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  63. def getNeighbors: List[A] =

    List(

    this.north.extract,

    this.east.extract,

    […]

    this.south.east.extract,

    this.south.west.extract

    )
    WALK THE NEIGHBORHOOD

    View Slide

  64. NEIGHBORHOOD
    LIFE CYCLE

    View Slide

  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

    View Slide

  66. WE NEED A
    COMONAD!

    View Slide

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

    w.value.focus.focus
    ???
    }
    A COMONAD FOR GRIDZIPPERS

    View Slide

  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

    View Slide

  69. View Slide

  70. View Slide

  71. NEIGHBORHOOD
    LIFE CYCLE

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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!

    View Slide

  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 …

    View Slide

  82. PICK THE RIGHT
    ABSTRACTION

    View Slide

  83. DEMO TIME!


    View Slide

  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

    View Slide