Reputation: 1205
Is this a sensible use of upper type bounds? I want a Team of different Employees, or a team of just one of the subtypes. Are there drawbacks to modeling it in this way?
sealed abstract class Employee {def salary: Double}
object Employee {
case class IndividualContributor(salary: Double, linesOfCode: Int) extends Employee
case class Manager(salary: Double, reports: List[Employee]) extends Employee
}
case class Team[T <: Employee](name: String, members: List[T])
My rationale is that I may have places where I use a team of any types of Employees, and other places where I need a specific type of Employee:
def teamSalaries(team: Team[Employee]) = team.members.map(_.salary).sum
def teamLinesOfCode(team: Team[IndividualContributor]) = team.members.map(_.linesOfCode).sum
Upvotes: 0
Views: 132
Reputation: 22850
So just for complementing these are other ways to model the same... but, spoiler, I still think that the type bound is still the best solution in this case.
final case class Team[+T](name: String, members: List[T])
def teamSalaries(team: Team[Employee]): Double = ???
def teamLinesOfCode(team: Team[IndividualContributor]): Double = ???
However, this allows creating teams of things that are not employees which is nonsense and IMHO, the idea (the joke) of using a statically typed language is to let the compiler catch those errors for you.
final case class Team[T](name: String, members: List[T])
def teamSalaries[T](team: Team[T])(implicit ev: T <:< Employee): Double = ???
def teamLinesOfCode[T](team: Team[T])(implicit ev: T <:< IndividualContributor): Double = ???
In this case, this is the same as the previous one. But, sometimes this is the only / best way to model something (see @Dmytro's answer for an example of such case).
sealed trait Employee {
def salary: Double
}
object Employee {
final case class IndividualContributor(salary: Double, linesOfCode: Int) extends Employee
final case class Manager(salary: Double, reports: List[Employee]) extends Employee
}
sealed trait Team {
def name: String
}
object Team {
final case class FullTeam(name: String, members: List[Employee]) extends Team
final case class OnlyDevelopersTeam(name: String, members: List[IndividualContributor]) extends Team
}
def teamSalaries(team: Team): Double = team match {
case Team.FullTeam => ???
case Team.OnlyDevelopersTeam => ???
}
def teamLinesOfCode(team: Team): Option[Double] = team match {
case Team.OnlyDevelopersTeam => Some(???)
case _ => None
}
But this just moves something that could have been checked by the compiler into the runtime, and imposes managing an unnecessary Option
. But, if the data is dynamic and you can not know at compile time which team you have then this is a good (best?) option.
trait HasSalary[T] {
def salary(t: T): Double
}
trait HasLinesOfCode[T] {
def linesOfCode(t: T): Double
}
sealed trait Employee {
def salary: Double
}
object Employee {
final case class IndividualContributor(salary: Double, linesOfCode: Int) extends Employee
final case class Manager(salary: Double, reports: List[Employee]) extends Employee
implicit EmployeeHasSalary: HasSalary[Employee] = (employee: Employee) => employee.salary
implicit IndividualContributorHasLinesOfCode: HasLinesOfCode[IndividualContributor] = (employee: IndividualContributor) => employee.linesOfCode
}
final case class Team[+T](name: String, members: List[T])
def teamSalaries[T : HasSalary](team: Team[T]): Double = ???
def teamLinesOfCode[T : HasLinesOfCode](team: Team[T]): Double = ???
But this is probably too much boilerplate for nothing. However, if your abstractions are general enough, are implemented for many unrelated types and the type of the data can be determined at compile time then nothing beats the flexibility & safety of a typeclass.
Upvotes: 3
Reputation: 51658
I'll object a little to @winson's answer :)
You could probably do without the type bound. While it is possible to have a
Team[Int]
, nobody would do that and the bounds might complicate a lot of other type signatures that are related to aTeam
as e.g. method parameter, ...
It's worth noting that type bounds are not just to exclude something external like Int
in this case. If you decide to refactor your code, either moving methods teamSalaries
and teamLinesOfCode
inside the case class Team
case class Team[T <: Employee](name: String, members: List[T]) {
def salaries = members.map(_.salary).sum
def linesOfCode(implicit ev: T <:< IndividualContributor) =
members.map(_.linesOfCode).sum
}
or defining them as extension methods
implicit class TeamOps[T <: Employee](val team: Team[T]) extends AnyVal {
def salaries = team.members.map(_.salary).sum
def linesOfCode(implicit ev: T <:< IndividualContributor) =
team.members.map(_.linesOfCode).sum
}
then without upper bounds the code will not compile.
Upvotes: 2
Reputation: 428
If I may share my experience here:
You could probably do without the type bound. While it is possible to have a Team[Int]
, nobody would do that and the bounds might complicate a lot of other type signatures that are related to a Team
as e.g. method parameter, ...
I advise you however, as Luis Miguel commented, to make Team
covariant.
One side related note: dependent on your domain you might implement teamSalaries
as totalSalary
method on Team
. If your domain experts talk about "total salary of a team" in this context it could make sense. DDD is about modeling and expressiveness (ubiquitous language)
Upvotes: 1