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.