Often, when programming, you'll have different degrees of information to you in different contexts.
For example, a web server may have two routes, which recieve information about a Person
, one of these routes receives a Person with a Name
and an id, and the other just receives a Name
.
We want to write code that looks something like this, but probably more complicated:
def lookupPersonAge(person: Person): Int
// this doesn't receive a name
def route1(person: Person): Response = {
complete(lookupPersonAge(person))
}
// this does receive a name
def route2(person: NamedPerson): Response = {
complete(s"${person.name} is ${lookupPersonAge(person)} years old")
}
It's tempting to model Person
as an interface, so that we can write generic code, that's usable between the two routes:
trait Person {
def id: Int
}
case class PersonID(id: Int) extends Person
case class NamedPerson(id: Int, name: String) extends Person
The problem with this representation, is that if we want to write code against the generic Person
interface, we loose a lot of useful things, such as pattern matching, and the auto generated copy method.
This also looks confusingly similar to an Algebraic Data Type, despite the fact that it shouldn't be used like one.
Because of this, it's appealing to model NamedPerson
using composition instead, for example
case class PersonId(id: Int)
case class NamedPerson(person: Person, name: String)
This is unwieldy, as it requires you to "lift" any methods written in terms of Person
, in order to apply them to NamedPerson
, in this case, we'd have to write route2
as:
def route2(person: NamedPerson): Response = {
complete(s"${person.name} is ${lookupPersonAge(person.person)} years old")
}
It also scales poorly, as different combinations of fields are added, or if either type of Person
may appear nested within another object.
For completeness, we could also model this using an Option
:
case class Person(id: Int, name: Option[String])
This isn't appealing, because we do know whether or not the name is going to be present, we want to write code that's generic to whether or not it is.
And this representation would lead us to write the following code, which requires us to use an unsafe call to get
def route2(person: NamedPerson): Response = {
complete(s"${person.name.get} is ${lookupPersonAge(person)} years old")
}
I feel like what I'm missing here is some form of structural typing, with that said, I'm sure I'm missing a nicer way to solve this kind of problem. What's an effective way to resolve this issue?