For this post I’ll consider the following simple case class unless otherwise specified:
case class Person(lastname: String, firstname: String, birthYear: Int)
In this post:
- Common knowledge about case classes
- Not so common knowledge about case classes
- Defining a case class using the curried form
- Defining a case class with a private constructor
- For the most curious ones
- Final Notes
Common knowledge about case classes
When you declare a case class the Scala compiler does the following for you:
- Creates a class and its companion object.
- Implements the
apply
method that you can use as a factory. This lets you create instances of the class without thenew
keyword. E.g.:
val p = Person("Lacava", "Alessandro", 1976)
// instead of the slightly more verbose:
val p = new Person("Lacava", "Alessandro", 1976)
- Prefixes all arguments, in the parameter list, with
val
. This means the class is immutable, hence you get the accessors but no mutators. E.g.:
val lastname = p.lastname
// the following won't compile:
p.lastname = "Brown"
- Adds natural implementations of
hashCode
,equals
andtoString
. Since==
in Scala always delegates to equals, this means that case class instances are always compared structurally. E.g.:
val p_1 = Person("Brown", "John", 1969)
val p_2 = Person("Lacava", "Alessandro", 1976)
p == p_1 // false
p == p_2 // true
- Generates a
copy
method to your class to create other instances starting from another one and keeping some arguments the same. E.g.: Create another instance keeping thelastname
and changingfirstname
andbirthYear
:
// the result is: Person(Lacava,Michele,1972), my brother :)
val p_3 = p.copy(firstname = "Michele", birthYear = 1972)
- Probably, most importantly, since the compiler implements the
unapply
method, a case class supports pattern matching. This is especially important when you define an Algebraic Data Type (ADT). E.g.:
sealed trait Maybe[+T]
case class Value[T](value: T) extends Maybe[T]
case object NoValue extends Maybe[Nothing]
val v: Maybe[Int] = Value(42)
val v_1: Maybe[Int] = NoValue
def logValue[T](value: Maybe[T]): Unit = value match {
case Value(v) => println(s"We have a value here: $v")
case NoValue => println("I'm sorry, no value")
}
// prints We have a value here: 42
logValue(v)
// prints I'm sorry, no value
logValue(v_1)
As you probably already know, when your class has no argument you use a case object
instead of a case class
with an empty parameter list.
Apart from being used in pattern matching the unapply
method lets you deconstruct a case class to extract
it’s fields, both during pattern matching and as a simple expression to extract some of its fields. E.g.:
val Person(lastname, _, _) = p
// prints Lacava
println(lastname)
Not so common knowledge about case classes
- What if you need a function that, given your case class arguments as parameters, creates an instance of the class? Here’s how you can do it by partially applying
apply
(no pun intended :)):
val personCreator: (String, String, Int) => Person = Person.apply _
// the result is: Person(Brown,John,1969)
personCreator("Brown", "John", 1969)
- What if you want your function, from the previous point, to be curried? Enters the
curried
method:
val curriedPerson: String => String => Int => Person = Person.curried
val lacavaBuilder: String => Int => Person = curriedPerson("Lacava")
val me = lacavaBuilder("Alessandro")(1976)
val myBrother = lacavaBuilder("Michele")(1972)
- What about obtaining a function that accepts a tuple whose arity is equal to the number of the case class arguments, and produces an instance of the class? Well, there’s the
tupled
method for that:
val tupledPerson: ((String, String, Int)) => Person = Person.tupled
val meAsTuple: (String, String, Int) = ("Lacava", "Alessandro", 1976)
val meAsPersonAgain: Person = tupledPerson(meAsTuple)
- You could also need a function that, given an instance of your class as input, produces an
Option[TupleN[A1, A2, ..., AN]]
as output, whereN
is the number of the case class arguments andA1, A2, ..., AN
are their types. E.g.:
val toOptionOfTuple: Person => Option[(String, String, Int)] = Person.unapply _
// Some((Lacava,Alessandro,1976))
val x: Option[(String, String, Int)] = toOptionOfTuple(p)
The curried
and tupled
methods are inherited from AbstractFunctionN
which is extended by the autogenerated
companion object. N
is the number of the case class formal parameters.
Note that, of course, if N = 1
you won’t get curried
and tupled
because
they wouldn’t make sense for just one parameter!
Defining a case class using the curried form
There’s another less-known way of defining a case class, e.g.:
case class Keyword(text: String)(source: String, foo: Int)
The formal parameters in the first parameter section of a case class (just text
in this case) are called elements;
they are treated specially. All the goodies you get when you define a case class (accessors, pattern matching support,
copy method, …) only apply to the first section. For example you don’t have an accessor for source
since
the compiler didn’t implicitly prefix it with val
, like it did for text
instead. E.g.:
val k1 = Keyword("restaurant")("storage", 1)
// won't compile
val source = k1.source
You can solve the accessor problem by prefixing the parameters with val
. E.g.:
case class Keyword(text: String)(val source: String, val foo: Int)
Anyway you still won’t get all the other case class features.
For instance, you cannot use the copy
method by specifying only the source
parameter. You have to specify, at least, all the parameters
of the sections successive to the first. E.g.:
// won't compile
val k2 = k1.copy()(source = "web")
// will compile
val k3 = k1.copy()(source = "web", foo = 1)
Finally, the companion object of a case class defined in such a way won’t extend AbstractFunctionN
, so the tupled
and curried
methods are not available.
At this point the natural question that may arise is: “Why on earth should I want to define a case class in such a way?”
Apparently there are cases when it could be a reasonable choice. For example suppose that, for your business model,
two instances of Keyword
are to be considered equal iff they have the same text
field. Well, in such a case by defining the
case class using the curried form you’ll get what you want. E.g.:
val k1 = Keyword("restaurant")("storage", 1)
val k2 = Keyword("restaurant")("web", 2)
// true!
k1 == k2
That’s because also the equals
implementation, you get for free for case classes, only applies to the first
parameter section, so only to text
in this case. I’m not saying here that this is always the best choice
but it could be of help in certain situations.
In fact, you could define your case class as usual and override equals
on your own. However overriding
equals
is not very trivial. Indeed, before doing that
I recommend you read the chapter 30 of Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition - Odersky, Spoon, Venners. Its title is Object Equality and it’s just 25 pages long!
Defining a case class with a private constructor
Sometimes you’ll want to control the construction of a case class so that you can perform a validation of the inputs at contruction-time. For example, Scala does not have a native way to define natural numbers (0, 1, 2, 3, …). In such a case you may be tempted to write something like this:
case class Nat private (value: Int)
object Nat {
def apply(value: Int): Option[Nat] =
if (value < 0) None else Some(new Nat(value))
}
Let’s give it a spin in the REPL:
scala> val x = Nat(42)
x: Option[Nat] = Some(Nat(42))
scala> val x = Nat(-42)
x: Option[Nat] = None
Nice, it seems to work. Unfortunately there’s a hole. As I stated at the beginning of the post, a case class automatically defines a copy
method for you and that could be used to subvert that private constructor:
scala> val x = Nat(42)
x: Option[Nat] = Some(Nat(42))
scala> val y = x.map(_.copy(value = -42)) // oops!
y: Option[Nat] = Some(Nat(-42))
The current solution is to define a private copy
method manually, so that the compiler won’t do it for us. Here is how to change the original definition:
case class Nat private (value: Int) {
private def copy(): Unit = ()
}
object Nat {
def apply(value: Int): Option[Nat] =
if (value < 0) None else Some(new Nat(value))
}
This way the previous workaround wouldn’t work:
scala> val x = Nat(42)
x: Option[Nat] = Some(Nat(42))
scala> val y = x.map(_.copy(value = -42)) // no way!
<console>:12: error: method copy in class Nat cannot be accessed in Nat
val y = x.map(_.copy(value = -42)) // no way!
However you’ll still have all the other goodies of case classes. For example pattern matching is available because the unapply
method is still automatically defined for you, being it a case class:
scala> x.foreach {
| case Nat(value) => println(s"Natural number: $value")
| }
Natural number: 42
The only two automatic definitions you disabled with this tecnique here are apply
and copy
.
That said, a much better alternative, in my opinion, is defining a sealed abstract case class
, as proposed by Rob Norris (@tpolecat) in this gist. That way you don’t even need to define the private copy
method. Plus the name fromInt
communicates more clearly the concept of a possible failure while one expects that apply
is a non-effectful method.
For the most curious ones
Furthermore, since each case class extends the Product
trait it inherits the following methods:
def productArity: Int
, returns the size of this product. In this case it corresponds to the number of arguments in the case class. E.g.:
val p = Person("Lacava", "Alessandro", 1976)
// equals to 3
val arity = p.productArity
def productElement(n: Int): Any
, returns the n-th element of this product, 0-based. In this case it corresponds to the n-th argument of the class. E.g.:
// Lacava
val lastname: Any = p.productElement(0)
-
def productIterator: Iterator[Any]
, returns an iterator over all the elements of this product which, in the case class context, they are its arguments. -
def productPrefix: String
, returns a string used in thetoString
method of the derived classes. In this case it’s the name of the class. E.g.:
// the result is Person
val className: String = p.productPrefix
Final Notes
-
I used type declarations in many expressions just to make things clearer. Of course I could have left them out and let the type inferer do its job.
-
Some
Product
’s methods return Any-based types, namelyproductElement
andproductIterator
. For example,p.productElement(0)
returns thelastname
but it is of typeAny
so if you need to use it asString
you have to cast it, which is an operation you should strive to avoid as much as possible. -
Product
extendsEquals
so every case class also inherits thecanEqual
method but, of course, going into its details is not the scope of this post. Besides, you don’t have to worry about it because it’s used interally by the autogeneratedequals
method, unless you decide to implement your own version ofequals
in which case you need to take into accountcanEqual
. Again, in such a case I strongly suggest you read the chapter cited here.