Type alias vs case class to add semantic meaning to data structures

Do you ever use type aliases instead of case classes to give existing data structures more meaningful names?

An example of tokenizers.

type FileToken = Array[ObjectToken]
type ObjectToken = Array[AttributeToken]
type AttributeToken = (String, String)

vs

case class FileToken(objects:Array[ObjectToken])
case class ObjectToken(attributes: Array[AttributeToken])
case class AttributeToken(key:String, value:String)

So actually there are three cases. Just use the data structures as they are, no type alias and no case class. Does not add any semantic meaning to the data structures but makes it quite clear what they actually are.

Add semantic meaning through type aliases or case classes? A bit more boilerplate using case classes. Any benefits or draw backs going either way?

type aliases are transparent. They give you information about intent, but won’t stop you from doing the wrong thing:

type Username = String
type Password = String

val user: Username = "user"
val password: Password = user //uh-oh

case classes don’t have this problem. They do add some overhead though, both syntactically and in terms of performance. Changing the first value of a List[List[(String, String)]] to “newValue” can be done as

def modifyFirstItem(fileToken: List[List[(String, String)]], newValue: String): List[List[(String, String)]] = 
  fileToken match {
    case ((k, _) :: attrTail) :: objTail => ((k, newValue) :: attrTail) :: objTail
    case _ => fileToken
  }

with type aliases, the signature cleans up and the implementation stays the same

def modifyFirstItem(fileToken: FileToken, newValue: String): FileToken = 
  fileToken match {
    case ((k, _) :: attrTail) :: objTail => ((k, newValue) :: attrTail) :: objTail
    case _ => fileToken
  }

with case classes, you need to wrap things all about

def modifyFirstItem(fileToken: FileToken, newValue: String): FileToken = 
  fileToken match {
    case FileToken(ObjectToken((AttributeToken(k, _) :: attrTail) :: objTail) =>
      FileToken(ObjectToken((AttributeToken(k, newValue) :: attrTail) :: objTail)
    case _ => fileToken
  }

This, depending on your perspective may help or hurt readability, but definitely is a lot more verbose.

Lenses (see e.g. https://julien-truffaut.github.io/Monocle/) may help with this, but whatever way you slice it, the additional semantic information comes with the cost of re-specifying this information in a lot of places.

Safety and convenience often have opposite demands and that’s the case here too.

3 Likes

There are some additional middle ground alternatives to the three cases, i.e. Value Classes and Tagged Types. As @martijnhoekstra notes, it’s a tradeoff to be assessed for each specific case. There has been a similar discussion in this forum recently.

Your specific example might raise the additional question whether you have an algebraic data type Token here, which would push the choice towards a sealed trait with case class/value class variants.

3 Likes

Thank you both.

Interesting, hadn’t stumbled over Tagged Types before. I thought about creating ADT:s, will probably do that if I go the case class route.

To summarize, for type safety, use case classes, ADT:s , value classes or tagged types, for just semantic information, use type aliases.