Introduction

Let’s create a simple case class with one String:

case class RealCaseClass(value: String)

The common wisdom is that case classes provide many useful features out of the box:

  • immutability
  • pretty printing (overridden toString)
  • comparison based on values (overridden equals and hashcode)
  • apply method in the companion object (to create instances without using new)
  • unapply method in the companion object (to help pattern matching)
  • copy method (to easily create copies mutating only a subset of fields)

When I started learning Scala I was also told that the case class is only a syntax sugar and that I could implement all of it myself. Today I would like to focus on the apply method and find the way to implement case-class-like applicative behavior in a normal class.

Implementation

In a real case class we can create a new instance without using the new keyword.

println(RealCaseClass("abc"))
//RealCaseClass(abc)

In Scala, using a method call syntax name(args) on an object or an instance of a class is just a shorter way to call the apply method:

println(RealCaseClass.apply("abc"))
//RealCaseClass(abc)

Knowing that we can start implementing our case class imitation. Let’s override the toString method so that we can print the value instead of Imitation@60f82f98 :-)

class Imitation(val value: String) {
  override def toString: String = s"Imitation($value)"
}

apply is a factory method and should be static so it should be implemented in the companion object:

object Imitation {
  def apply(value: String): Imitation = new Imitation(value)
}

Now we can try to use it:

println(Imitation("abc"))
//Imitation(abc)

Yeah! It works. Turns out though that there is still one thing related to the apply method the original case class can do and the imitation cannot.

Seq("a", "b", "c").map(RealCaseClass).foreach(println)
//RealCaseClass(a)
//RealCaseClass(b)
//RealCaseClass(c)

Seq("a", "b", "c").map(Imitation).foreach(println)
//Compilation error: type mismatch; found: Imitation.type, required: String => ? 

The map function called on a sequence of strings expects a function that maps String to any type (String => ?) as an argument. Scala has no duck typing, so even though Imitation.apply meets the requirements, we still have to explicitly tell the compiler that Imitation.type indeed implements String => Imitation.

The error also means that the compiler adds something to RealCaseClass that we haven’t got in Imitation yet. To see what it is we can use the “Java Class File Disassembler”, javap (usually included with JDK). Let’s compare the class files for both companion objects (generated using Scala 2.11.7):

javap Imitation$.class
Compiled from "CaseClasses.scala"
public final class Imitation$ {
  public static final Imitation$ MODULE$;
  public static {};
  public Imitation apply(java.lang.String);
}
javap RealCaseClass$.class
Compiled from "CaseClasses.scala"
public final class RealCaseClass$ extends scala.runtime.AbstractFunction1<java.lang.String, RealCaseClass> implements scala.Serializable {
  public static final RealCaseClass$ MODULE$;
  public static {};
  public final java.lang.String toString();
  public RealCaseClass apply(java.lang.String);
  public scala.Option<java.lang.String> unapply(RealCaseClass);
  public java.lang.Object apply(java.lang.Object);
}

We can see that the real case class companion object has the apply method with the same signature as our imitation. There is also the second apply method Object => Object – let’s ignore it for now. What our Imitation companion object needs to work with the map function is in this part:

extends scala.runtime.AbstractFunction1<java.lang.String, RealCaseClass>

Here’s our working version of the case-class-like apply imitation:

class Imitation(val value: String) {
  override def toString: String = s"Imitation($value)"
}

object Imitation extends AbstractFunction1[String, Imitation]{
  def apply(value: String): Imitation = new Imitation(value)
}
Seq("a", "b", "c").map(Imitation).foreach(println)
//Imitation(a)
//Imitation(b)
//Imitation(c)

Appendix 1: Bridge Methods

When we run javap again on the new version of Imitation$.class the output is:

Compiled from "CaseClasses.scala"
public final class Imitation$ extends scala.runtime.AbstractFunction1<java.lang.String, Imitation> {
  public static final Imitation$ MODULE$;
  public static {};
  public Imitation apply(java.lang.String);
  public java.lang.Object apply(java.lang.Object);
}

We can see that the second apply method (Object => Object) was added automatically by the compiler when we extended AbstractFunction1. That’s the way Java deals with type erasure of generic type arguments. After compilation generic type arguments of AbstractFunction1 are lost so the apply method defined in that class becomes public Object apply(Object). To override it in Imitation$ the compiler needs to generate the second apply method there with the same signature. Those methods are called the bridge methods. If we run javap -c to display the code we can see that all the second apply does is casting and invoking the ‘original’ method.

Appendix 2: AbstractFunctionN

In the source code of scala.runtime.AbstractFunction1 we can see that it extends the scala.Function1 trait without adding anything. I guess it may exist for performance reasons or better interoperability with Java, I’m not sure. When

object Imitation extends AbstractFunction1[String, Imitation]

is changed to

object Imitation extends Function1[String, Imitation]

Intellij Idea displays an inspection tooltip:

syntactic sugar could be used. Detects explicit references to FunctionN and TupleN that could be replaced with syntactic sugar.

Turns out that A => B type that is required as an argument to the map function is just a syntactic sugar for Function1[A, B]. When we apply the proposed change we end up with a rather intuitive

object Imitation extends ((String) => Imitation){
  def apply(value: String): Imitation = new Imitation(value)
}

and everything still works fine. If we run javap Imitation$.class again we can see that over 20 different specialized apply methods has been copied from the trait into the class bytecode. That’s why I thought that case classes may extend AbstractFunctionN instead of FunctionN to improve the performance.

Conclusion

Scala does a lot of work in the background to make sure we can use case classes in a pleasant way. Case class “behaves like a function” because the Scala compiler generates a companion object for that class that

  • has an apply method so that we can call MyCaseClass(args) to create a new instance
  • extends AbstractFunctionN so that we can call collection.map(MyCaseClass) to create multiple instances at once