Is your Scala object always a singleton?

Jacek Kunicki
SoftwareMill Tech Blog
5 min readFeb 18, 2019

--

You’re probably not surprised that the answer is it depends.

In this article I’m going to present a root cause analysis of a bug we encountered in one of our projects — the bottomline was having more than one instance of a Scala object, which was supposed to be a singleton.

The pattern matching

The story began with pattern matching on an ADT like:

It worked like a charm until, after introducing an apparently unrelated change, we started getting errors like:

scala.MatchError: Some(Slack) (of class scala.Some)

The error seemed a bit strange, since this was a really simple pattern matching which should just have worked. Also, the matching on Email still worked fine. And since the Slack object was supposed to be a singleton, we didn’t really have any clue how could it possibly not be matched.

But somehow it was — so let’s unveil some more facts.

The mapper

The only idea we came up with was that there had to be more than one instance of Slack — which we were able to confirm by looking at the hashcodes in the debugger.

Initially, we suspected some classloading issue, but since the application was a standalone one and didn’t run inside any container, this was hardly possible. Checking the classloaders with the debugger confirmed that this was a red herring.

Now comes the key fact: the values received by Notifier.sendNotification were not provided directly by our application code, but instead read from some configuration stored in MongoDB.

Additionally, we were using Morphia as the document-to-object mapper, which was responsible for providing the actual instances of the implementations of Notification.

To complicate things even more, in our case the Notification was a property of two different objects, say A and B, so its instances could be a result of mapping documents from two different collections. Now the funny thing was that A.notification was matched correctly, but B.notification was not (or the other way around, but still only one of those worked fine while the other didn’t).

The obvious next step was to dig into the source code of Morphia and check how it mapped the documents to Scala objects.

The reflection

It turned out that Morphia uses Java reflection to instantiate the objects fetched from MongoDB:

Since Morphia is a Java library, it has no way to be aware of something like a Scala object — it just creates a new instance of whatever class it encounters.

Also, from the JVM perspective, an object is just a class that, actually, has a private constructor, so nothing can stop you (or Morphia) from manually creating a new instance using reflection.

Let’s now digress a bit from the main story and see what a compiled object actually looks like.

The anatomy of an object

If you compile the Notification ADT, you’ll get a couple of resulting classes:

$ scalac Notifications.scala$ ls -1 *.class
Email$.class
Email.class
Notification.class
Slack$.class
Slack.class

If you have never seen a compiled object before, you may be surprised by the fact that the compiler not only generated Slack.class (which you would probably expect), but also Slack$.class.

Let’s inspect those two classes with javap — a command line tool in the JDK that lets you disassemble class files:

$ javap -p Slack.class
Compiled from "Notifications.scala"
public final class Slack {
public static java.lang.String toString();
public static int hashCode();
public static boolean canEqual(java.lang.Object);
public static scala.collection.Iterator<java.lang.Object> productIterator();
public static java.lang.Object productElement(int);
public static int productArity();
public static java.lang.String productPrefix();
}
$ javap -p Slack\$.class
Compiled from "Notifications.scala"
public final class Slack$
implements Notification,scala.Product,scala.Serializable {
public static final Slack$ MODULE$;
public static {};
public java.lang.String productPrefix();
public int productArity();
public java.lang.Object productElement(int);
public scala.collection.Iterator<java.lang.Object> productIterator();
public boolean canEqual(java.lang.Object);
public int hashCode();
public java.lang.String toString();
private java.lang.Object readResolve();
private Slack$();
}

If you took a deeper dive into Slack.class with javap -c (which shows the actual bytecode), you would notice that this class is only a helper whose static methods delegate to the respective ones in Slack$.

Now in the Slack$ class, at the very end, you can notice the actual private constructor that can’t be used with new (since it’s private), but can still be called using reflection.

The other crucial part of the Slack$ class is the static MODULE$ field — which turns out to be the singleton instance created internally by calling the private constructor.

As long as you access the object in your Scala code using Slack, or in your Java code using Slack$.MODULE$ (which looks ugly but is the way to go), it remains a singleton. However, as you already know, nothing can prevent you from creating another instances using reflection.

Also, if you look deeper into Slack$ with javap -c, you will notice that the MODULE$ static field is initialized every time the private constructor is called:

private Slack$();
Code:
0: aload_0
1: invokespecial #64 // Method java/lang/Object."<init>":()V
4: aload_0
5: putstatic #63 // Field MODULE$:LSlack$;
8: aload_0
9: invokestatic #68 // InterfaceMethod scala/Product.$init$:(Lscala/Product;)V
12: return

This is the reason why only some instances (to be precise — only the most recently created one) are matched and the others are not. Kudos to Kamil Rafałko for pointing this out.

The solution

Coming back to our case with reading the Notifications from MongoDB, it should now be clear why the pattern matching didn’t work: a new instance of Slack was created every time the value was deserialized.

To fix it, we decided to write a custom TypeConverter that Morphia would use when deserializing Slack objects, which would always provide the singleton instance instead of creating a new one every time:

The idea behind the custom converter is to handle the special case of deserializing the Slack value and falling back to default behavior in any other case.

The summary

Although under normal circumstances the objects in Scala are indeed singletons, this can no longer be the case when reflection comes into play. From the reflection perspective, an object is nothing more than any other class with a private constructor.

Moreover, when there’s more than a single instance of an object, their behavior can become pretty weird — check the examples below and see for yourself.

The riddle

Can you tell what would be the result of running the code below? Try it in the Scala REPL or in Ammonite (which is a more powerful REPL with syntax highlighting). Is the output as you expected? Is it deterministic?

Looking for Scala and Java Experts?

Contact us!

We will make technology work for your business. See the projects we have successfully delivered.

--

--