850

# Comonads and the game of life

Slides from my NE Scala talk. March 13, 2020

## Transcript

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

8. THIS IS NOT A

9. BUT…

▸ Lift values into a context
▸ Chain computations which happen in a context
▸ Stop immediately on failures

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

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

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

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

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

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

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

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

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

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

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

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

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

32. 1 2 3 …

MOVE RIGHT

33. 1 2 3 …

MOVE RIGHT

▸ The focus allows us to call extract
▸ Coﬂatten creates a Zipper of all possible Zippers
▸ Every element is in focus once

???

}
}

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

???
}

38. 1 2 3 …

EXTRACT

39. 1
2
3 …

EXTRACT

???

override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] =
Zipper(w.duplicateLefts, w, w.duplicateRights)
}

41. override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] =
Zipper(w.duplicateLefts, w, w.duplicateRights)
NEW FOCUS

42. override def coflatten[A](w: Zipper[A]): Zipper[Zipper[A]] =
Zipper(w.duplicateLefts, w, w.duplicateRights)
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))
)

44. def duplicateLefts[B]: Stream[Zipper[A]] =
unfold(this)((z: Zipper[A]) =>
z.maybeLeft.map((x: Zipper[A]) => (x, x))
)
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))
)
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

override def extract[A](w: GridZipper[A]): A =
w.value.focus.focus
???
}

???
override def coflatten[A](w: GridZipper[A]): GridZipper[GridZipper[A]] =
map(GridZipper(nest(nest(w.value))))(GridZipper(_))
}

69. NEIGHBORHOOD
LIFE CYCLE

70. 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
ALIVE CELL = 1

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

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

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
CELL IS ALIVE, 2 OR 3 NEIGHBORS
STAY ALIVE

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
3 ALIVE NEIGHBORS
CELL BECOMES ALIVE

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
ALL OTHER CASES,
AN ALIVE CELL DIES

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

78. 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!

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

80. PICK THE RIGHT
ABSTRACTION

81. DEMO TIME!

82. POSTSCRIPT
▸ Link to source code: https://github.com/rlmark/