Lately I’ve been looking for ways to make complicated Scala code easier to read. One way I have found to do this is with custom types. Custom types are especially useful when dealing with a complicated business domain.

Here’s a quick & contrived example of what I mean:

sealed trait GameOutcome
case object Won extends GameOutcome
case object Lost extends GameOutcome

case class Player(name: String)
case class Score(amount: Int)
case class Game(player: Player, score: Score)
case class GameHistory(value: Map[GameOutcome, List[Game]])

val p1 = Player("player1")

val s1 = Score(10)
val s2 = Score(20)
val s3 = Score(30)

val g1 = Game(p1, s1)
val g2 = Game(p1, s2)
val g3 = Game(p1, s3)

val gameHistory = GameHistory(Map(Won -> List(g1, g2), Lost -> List(g3)))

What’s the point here? GameHistory is just a wrapper around a Map? Why are we wrapping simple Scala primitives in excessive case classes?

The point here is to achieve simplicity and avoid complexity. I personally think that custom types are a way to achieve that - the reader can observe the Types in play and then not have to wonder about the concepts behind the primitives.

For example, let’s add some methods to our GameHistory, playing with Scala’s apply concept where a function/method is applied to a parameter value, and also lets extend the standard ++ operator from the underlying Map class.

case class GameHistory(value: Map[GameOutcome, List[Game]]) {
  // hack to get at the value of the GameHistory cleanly
  def apply(): Map[GameOutcome, List[Game]] = value
  // use the native Map functionality to get at the value without extra code, i.e gameHistory.value(x)
  def apply(x: GameOutcome): List[Game] = value(x)
  // use our new "parameter-less" apply method to combine two maps
  def ++(x: GameHistory): GameHistory = new GameHistory(value ++ x())
}

val gameHistory = GameHistory(Map(Won -> List(g1, g2)))
val moreHistory = GameHistory(Map(Lost -> List(g3)))
val combinedGameHistory = gameHistory ++ moreHistory

combinedGameHistory()
// Map(
//  Won -> List(Game(Player("player1"), Score(10)), Game(Player("player1"), Score(20))),
//  Lost -> List(Game(Player("player1"), Score(30)))
// )

combinedGameHistory(Won)
// List(Game(Player("player1"), Score(10)), Game(Player("player1"), Score(20)))

This can be dangerous however, and introduce too much complexity and convulsion. I have to remember to be careful and use this technique only when it actually is the simplest way to solve a problem