allstar
allstar

Reputation: 1205

Scala domain modeling with upper type bounds

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

Answers (3)

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.

Normal overloading

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.

Implicit evidence

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).

And ADT

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.

A typeclass

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

Dmytro Mitin
Dmytro Mitin

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 a Team 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

winson
winson

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

Related Questions