Question

Let's say I have the following:

trait Person {
  val name: String
}
case class Student(val name: String) extends Person
case class Teacher(val name: String, students: List[Student]) extends Person

I'd like a function function that could take any Person implementation, match on the specific type, and then return the most specific type possible. (I know this might not be the smartest thing, but bear with me.) Let's say something like:

def teacherGreeting(teacher: Teacher): (Teacher, String) = {
  val names = teacher.students.map(_.name).mkString(", ")
  (teacher, s"Hello ${teacher.name}, your students are $names")
}

def greet[P <: Person](person: P): (P, String) = person match {
  case Student(name) => (person, s"Hello $name")
  case Teacher(name, students) => teacherGreeting(person)
}

But then I get:

<console>:19: error: type mismatch;
 found   : P
 required: Teacher
             case Teacher(name, students) => teacherGreeting(person)
                                                         ^

If I have the logic of teacherGreeting inside of greet, I don't have any problems. So, why doesn't the compiler know that P in this branch of the code must be a Teacher?

If I use the matched value:

def greet[P <: Person](person: P): (P, String) = person match {
  case Student(name) => (person, s"Hello $name")
  case teacher @ Teacher(name, students) => teacherGreeting(teacher)
}

The error just happens later, with the result of teacherGreeting, instead of the input:

error: type mismatch;
 found   : (Teacher, String)
 required: (P, String)
             case teacher @ Teacher(name, students) => teacherGreeting(teacher)
                                                                  ^

Is there no way to avoid casting?

Was it helpful?

Solution

When you compile greet(p), the compiler has to infer the type P in

def greet[P <: Person](person: P): (P, String)

If you then defined a subclass of Person and call greet on that an instance:

class Parent(val name: String) extends Person
val parent = new Parent("Joe")
val (p, greeting) = greet(parent)

The inferred type P is determined at compile time, it does not depend on the runtime type of parent. So the compiler would have to infer P as Parent: greet[Parent](parent).

But then how would these expressions be typed? In order to type check, since P is Parent, they have to be of type (Parent, String):

case teacher @ Teacher(name, students) => teacherGreeting(teacher)
case Teacher(name, students) => teacherGreeting(person)

In the first case, the return type of teacherGreeting(teacher) is (Teacher, String). And Teacher is not a Parent. The return type does not match.

In the second case, you are calling teacherGreeting(person: Parent) so it is wrong type for the argument.

When you say you don't have a problem if you inline the body of teacherGreeting inside the second case, that is probably because you return (person, "str"). And in that case person would be of type P for sure. But when you pass it through teacherGreeting, the compiler does not know that you are returning the passed argument of type P. For all we know you could be returning Teacher("another", List()).

Edit: so thinking on how to preserve the type P here is a (cumbersome) way to go about it. You want to retain the type through the teacherGreeting call. This can be done like this. Use a type parameter Q that will be inferred as the P from greet:

def teacherGreeting[Q <: Teacher](teacher: Q): (Q, String) = {
  val names = teacher.students.map(_.name).mkString(", ")
  (teacher, s"Hello ${teacher.name}, your students are $names")
}  

Tell the compiler that teacher is a P and a Teacher:

def greet[P <: Person](person: P): (P, String) = person match {
  case Student(name) => (person, s"Hello $name")
  case teacher: (P with Teacher) => teacherGreeting(teacher)
} 

OTHER TIPS

Actually it can be even shorter, cause i don't see any reason in unapplying teacher in PatMat:

def greet(person: Person): (Person, String) = person match {
  case Student(name)    => (person, s"Hello $name")
  case teacher: Teacher => teacherGreeting(teacher)
}

You can use type classes for this:

trait Greeter[P <: Person] {
  def greet(person: P): (P, String)
}

object Greeter {
  implicit object studentGreeter extends Greeter[Student] {
    def greet(student: Student) = (student, s"Hello ${student.name}")
  }
  implicit object teacherGreeter extends Greeter[Teacher] {
    def greet(teacher: Teacher) = {
      val names = teacher.students.map(_.name).mkString(", ")
      (teacher, s"Hello ${teacher.name}, your students are $names")
    }
  }
}

def greet[P <: Person](person: P)(implicit gr: Greeter[P]) = gr.greet(person)

A note aside: You don't really need the type-bound on Person, it's rather for documentation / prevention of abuse.

Like the other answer, I wanted to motivate the error message.

Curiously, the inferred type is happy for the extractor pattern and unhappy with the constructor pattern (that is, if Teacher is a case class, where it sees t.type instead of P).

package teachers

trait Person {
  def name: String
  override def toString = name
}
case class Student(name: String) extends Person
//case class Teacher(name: String, students: List[Student]) extends Person
class Teacher(val name: String, val students: List[Student]) extends Person
object Teacher {
  def apply(name: String, students: List[Student]) = new Teacher(name, students)
  def unapply(teacher: Teacher) = Some((teacher.name, teacher.students))
}
class Substitute(name: String, students: List[Student]) extends Teacher(name, students)
object Substitute {
  def apply(name: String, teacher: Teacher) = new Substitute(name, teacher.students)
  def unapply(sub: Substitute) = Teacher.unapply(sub)
}

object Test extends App {
  def teacherGreeting[A <: Teacher](teacher: A, duration: String): (A, String) = {
    val names = teacher.students.map(_.name).mkString(", ")
    (teacher, s"Hello ${teacher.name}, your students for the $duration are $names")
  }

  def greet[P <: Person](person: P): (P, String) = person match {
    case Student(name)                  => (person, s"Sit down and be quiet, $name")
    case s @ Substitute(name, students) => teacherGreeting(s, "day")
    case t @ Teacher(name, students)    => teacherGreeting(t, "year")
  }
  import reflect.runtime.universe._
  def show[P <: Person : TypeTag](person: P) = implicitly[TypeTag[P]].tpe.typeSymbol.name

  val mary = Teacher("Mary", List("Dick","Jane").map(Student))
  val (who, msg) = greet(Substitute("Bob", mary))
  Console println s"$who is a ${show(who)}"
  Console println msg
}

I'm thinking you don't really need a generic type on greet. When you change your greet to:

def greet(person: Person): (Person, String) = person match {
  case Student(name) => (person, s"Hello $name")
  case teacher @ Teacher(name, students) => teacherGreeting(teacher)
}

Everything works just fine.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top