types for your health
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