matanox
matanox

Reputation: 13716

Refactoring a case class to a non-case class without failing compilation

Fiddling with sample code demonstrating type bounds, I changed the original code below from using a case class to a plain class, for the definition of class MyInt, the only class in this snippet.

trait Similar {
  def isSimilar(x: Any): Boolean
}

class MyInt(x: Int) extends Similar {
  def isSimilar(m: Any): Boolean =
    m.isInstanceOf[MyInt] &&
    m.asInstanceOf[MyInt].x == x
}

object UpperBoundTest extends App {
  def findSimilar[T <: Similar](e: T, xs: List[T]): Boolean =
    if (xs.isEmpty) false
    else if (e.isSimilar(xs.head)) true
    else findSimilar[T](e, xs.tail)
  val list: List[MyInt] = List(MyInt(1), MyInt(2), MyInt(3))
  println(findSimilar[MyInt](MyInt(4), list))
  println(findSimilar[MyInt](MyInt(2), list))
}

That no longer compiles

[error] 7: type mismatch;
[error]  found   : MyInt
[error]  required: ?{def x: ?}
[error] Note that implicit conversions are not applicable because they are ambiguous:
[error]  both method any2Ensuring in object Predef of type [A](x: A)Ensuring[A]
[error]  and method any2ArrowAssoc in object Predef of type [A](x: A)ArrowAssoc[A]
[error]  are possible conversion functions from MyInt to ?{def x: ?}
[error]     m.asInstanceOf[MyInt].x == x
[error]                   ^

This is an interesting case to me, as sometimes I find myself refactoring case classes to plain classes while refactoring inheritance, and clearly some changes need to made in the code in order to enjoy a smooth transition that preserves the code working.

Switching .AsInstanceOf to an equivalent match (m match {case m:MyInt => m.x == x; case _ => false}) yields the same compilation error. Using a more naive match does not compile either:

trait Similar {
  def isSimilar(x: Any): Boolean
}

class MyInt(x: Int) extends Similar {
  def isSimilar(m: Any): Boolean = {
    m match {case m:MyInt => true; case _ => false}
  }
}

object UpperBoundTest extends App {
  val a = new MyInt(4)
  def findSimilar[T <: Similar](e: T, xs: List[T]): Boolean =
    if (xs.isEmpty) false
    else if (e.isSimilar(xs.head)) true
    else findSimilar[T](e, xs.tail)
  val list: List[MyInt] = List(MyInt(1), MyInt(2), MyInt(3))
  println(findSimilar[MyInt](MyInt(4), list))
  println(findSimilar[MyInt](MyInt(2), list))
}

18: not found: value MyInt
[error]   val list: List[MyInt] = List(MyInt(1), MyInt(2), MyInt(3))
[error]                                ^
18: not found: value MyInt
[error]   val list: List[MyInt] = List(MyInt(1), MyInt(2), MyInt(3))
[error]                                          ^
18: not found: value MyInt
[error]   val list: List[MyInt] = List(MyInt(1), MyInt(2), MyInt(3))
[error]                                                    ^
19: not found: value MyInt
[error]   println(findSimilar[MyInt](MyInt(4), list))
[error]                              ^
20: not found: value MyInt
[error]   println(findSimilar[MyInt](MyInt(2), list))
[error]                              ^
[error] 5 errors found

Are non-case classes a practice to be enitrely discouraged other than using them for inheriting them, or when you will never need to check their type? when would you ever use a non-case class?

Upvotes: 1

Views: 293

Answers (2)

Alexey Romanov
Alexey Romanov

Reputation: 170815

Complete code you need to avoid compilation failure in any case, including pattern matching:

class MyInt(val x: Int) extends Similar

object MyInt {
  def apply(x: Int) = new MyInt(x)

  // if more than one field, return Some(tuple of all fields)
  def unapply(myInt: MyInt): Option[Int] = Some(myInt.x)
}

If you also want behavior to be the same as original, you need to define equals and hashCode methods as well:

class MyInt(val x: Int) extends Similar {
  def equals(y: Any) = y match {
    case MyInt(z) => x == z
    case _ => false
  }

  def hashCode = x
}

Upvotes: 2

Gabriele Petronella
Gabriele Petronella

Reputation: 108151

Simply declare your class as

class MyInt(val x: Int) extends Similar

For case classes val is the default, whereas on regular classes you need to explicitly add it to signal that you want to synthesize the accessors for the constructor parameters

Also, in case classes a companion object providing a default apply method is automatically synthesized, allowing you to call

MyInt(2)

as opposed to

new MyInt(2)

You either have to use the latter, or manually provide an apply method in a companion object.

Bottom line, if you need to match on a class, case classes are much more convenient, as you can skip the downcast (which is implicitly performed when you match on the type doing x: MyInt)

Classes are not discouraged, simply case-classes are much more convenient in your specific use -case

Upvotes: 5

Related Questions