Saturday, February 1, 2014

Rules to write functional Scala code and stay sane

Use only cases classes from the OOP world

Well, with some exceptions that will follow soon. But the baseline is: no regular classes, no inheritance. A case class can be considered an abstract data type, so it can serve as a container. Another use case for them can be seen as a little weird form of currying.

val people = List("Bob", "Mary")

// functional
def join(separator: String, list: List[String]): String = {
  list mkString separator
}

def joiner(separator: String) = {
  (list: List) => join(separator, list)
}

joiner(",")(people)

// with a case class
case class Joiner(separator: String) {
  def join(list: List[String]) = {
    list mkString separator
  }
}

Joiner(",") join people

Use traits only to simulate union types

There are cases when an instance can be one of two types, for example a tree node can be either a branch, or a leaf node. If we follow the above rule of using only case classes, we’ll be in trouble. This is where we can use a trait shared by some case classes.

trait TreeNode {}

case class Leaf(value: String) extends TreeNode {}

case class Branch(left: TreeNode, right: TreeNode) extends TreeNode {}
It’s OK to add methods to the trait if they would be shared by the case classes. (It would be difficult to use this tree example, because that would lead us to the field of recursive data types.)
trait NDArray {
  def rank: Int
  def isScalar = {
    rank == 0
  }
}

case class Scalar(value: Double) extends NDArray {
  def rank: Int = 0
}

case class Matrix(elems: List[Any]) extends NDArray {
  def rank: Int = {
    elems.headOption match {
      case Some(x: NDArray) => 1 + x.rank
      case None => 1
      case Some(_) => 1
    }
  }
}

Matrix(List(4)).rank // => 1
Matrix(List(Matrix(List(5)))).rank // => 2

Scalar(5).isScalar // => true
Matrix(List(4)).isScalar // => false

Avoid class level values

It’s a sign that you are about to add an unwanted dependency which will be difficult to inject/mock later. A typical usage to avoid is reading configuration values in the middle of some method.

case class BadGreeting(firstName: String, lastName: String) {
  val person = Person(firstName, lastName)
  val location = Configurator getConfigValue "greeting.location"

  def greetWith(greeting: String) = {
    greeting + ", " + person.fullName + " in " + location
  }
}

// That should be refactored to this class
case class GoodGreeting(person: Person, location: String) {
  def greetWith(greeting: String) = {
    greeting + ", " + person.fullName + " in " + location
  } 
}

Pass an argument object instead of too many arguments

The rules so far prefer explicit to implicit. It may lead to a loooong list of arguments, though. The cure for this is to create a case class for those arguments. It’s a matter of personal taste how many arguments you consider too many. I would say, don’t have more than three.

No comments:

Post a Comment