2
\$\begingroup\$

I have been doing OOP and imperative programming my whole career but I wanted to dabble in Scala along with functional programming. Rather than writing Java like code in Scala I wanted to try a more functional approach. I decided to try a simple Kata with Scala, Game of Life. The following is what I came up with. I admit I am very much stuck in OOP/imperative mindset. I am not sure if this is the best approach for Scala, or functional programming. I avoided functions that mutate objects, and favor returning a new copy. I was unsure what the best approach is with functional programming to ensure objects can't be created in an invalid state. Is there a way I can make my code more aligned with idiomatic Scala and functional programming.

package io.jkratz.katas.gameoflife

import scala.util.Random

case class Board(grid: Array[Array[Int]]) {

  require(grid != null, "grid cannot be null")
  require(!isJagged(grid), "grid cannot be jagged")
  require(isValid(grid), "grid contains invalid values, 0 and 1 are the only valid values")

  val rows: Int = {
    grid.length
  }

  val columns: Int = {
    grid(0).length
  }

  def evolve(): Board = {

    val newGrid = Array.ofDim[Int](rows, columns)
    for (i <- grid.indices) {
      for (j <- grid(0).indices) {
        newGrid(i)(j) = getNextCellState(i,j)
      }
    }

    Board(newGrid)
  }

  private def getNextCellState(i:Int, j: Int): Int = {
    var liveCount = 0
    val cellValue = grid(i)(j)
    for (x <- -1 to 1; y <- -1 to 1) {
      if (i + x < 0 || i + x > (this.rows - 1) || y + j < 0 || y + j > (this.columns - 1)) {
        // do nothing, out of bounds
      } else {
        liveCount += grid(i + x)(j + y)
      }
    }
    liveCount -= cellValue

    if (cellValue.equals(Board.CELL_ALIVE) && (liveCount < 2 || liveCount > 3)) {
      Board.CELL_DEAD
    } else if (cellValue.equals(Board.CELL_DEAD) && liveCount == 3) {
      Board.CELL_ALIVE
    } else {
      cellValue
    }
  }

  private def isJagged(grid: Array[Array[Int]]): Boolean = {
    var valid = true
    val size = grid(0).length
    grid.foreach(row => if (row.length.equals(size)) valid = false)
    valid
  }

  private def isValid(grid: Array[Array[Int]]): Boolean = {
    var valid = true
    for (i <- grid.indices; j <- grid(0).indices) {
      val x = grid(i)(j)
      if (x != 0 && x != 1) {
        valid = false
      }
    }
    valid
  }
}

object Board {

  val CELL_DEAD = 0
  val CELL_ALIVE = 1
  val DEFAULT_ROWS = 10
  val DEFAULT_COLUMNS = 10

  def random(rows: Int = DEFAULT_ROWS, columns: Int = DEFAULT_COLUMNS): Board = {
    val grid = Array.ofDim[Int](rows, columns)
    for (i <- grid.indices) {
      for (j <- grid(0).indices) {
        grid(i)(j) = Random.nextInt(2)
      }
    }
    Board(grid=grid)
  }

  def prettyPrint(board: Board): Unit = {
    val grid = board.grid
    for (i <- grid.indices) {
      for (j <- grid(0).indices) {
        if (grid(i)(j) == 0) print(" - ") else print(" * ")
      }
      println()
    }
  }
}

And my main entry point.

package io.jkratz.katas.gameoflife

object Life {

  def main(args: Array[String]): Unit = {

    val state0 = Board.random(5,5)
    println("State 0")
    Board.prettyPrint(state0)

    val state1 = state0.evolve()
    println("State 1")
    Board.prettyPrint(state1)
  }
}
\$\endgroup\$
2
\$\begingroup\$

You're doing a lot of indexing, which is efficient in an Array, but indicates that you're thinking in small steps. As you get to know the Scala Standard Library you start thinking in larger chunks because it offers many ways to process data collections all at once.

I'll start at the bottom and work my way up.

*prettyPrint() - Testing for value 0 means that the prettyPrint() method knows about the underlying representation. If you test for CELL_DEAD and/or CELL_ALIVE then prettyPrint() should still work even if the grid implementation changes.

Here I chose to turn each row into a String and then println() it.

def prettyPrint(board: Board): Unit =
  board.grid
       .map(_.map(c => if (c == CELL_DEAD) " - " else " * ").mkString)
       .foreach(println)

*random() - Most Scala collections offer many different "builder" methods on the companion object. Here I use fill() to populate a 2-dimensional Array.

def random(rows:Int = DEFAULT_ROWS, columns:Int = DEFAULT_COLUMNS): Board =
  Board(Array.fill(rows,columns)(Random.nextInt(2)))

*isValid() - The Standard Library doesn't offer many collection methods with early termination, but forall() is one of them. It stops after the first false encountered.

Here I use -2 as a bit-mask to test the value of all bits except for the lowest.

private def isValid(grid: Array[Array[Int]]): Boolean =
  grid.forall(_.forall(n => (n & -2)==0))

*isJagged() - The exists() method is the compliment of forall(). It stops after the first true encountered.

private def isJagged(grid: Array[Array[Int]]): Boolean =
  grid.exists(_.length != grid.head.length)

*liveCount - Idiomatic Scala avoids mutable variables. In order to calculate the value of liveCount once, without any post-evaluation adjustments, we'll want a way to get all valid neighbor-cell indexes, also without any post-evaluation adjustments.

val liveCount = (for {
  x <- (0 max i-1) to (i+1 min rows-1)
  y <- (0 max j-1) to (j+1 min columns-1)
} yield grid(x)(y)).sum - cellValue

*evolve() - tabulate() is another one of those "builder" methods that appears to do everything you need, in this situation, all at once. In this case, because we're building a 2-D Array, tabulate() passes two arguments, the row index and the column index, to its lambda argument. And because the getNextCellState() method takes those same two arguments in the same order, they don't need to be explicitly specified.

def evolve(): Board = Board(Array.tabulate(rows,columns)(getNextCellState))

It's worth noting that you test if grid is null but you don't test to see if it's empty. Board.random(0,0) will throw a run-time error.

\$\endgroup\$
  • \$\begingroup\$ Wow this is a great answer and well explained! Thanks for taking the time to provide a very detailed answer. \$\endgroup\$ – jkratz55 Dec 18 '18 at 13:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.