Value Classes

Code like 2 days ago is charming, but what’s the catch? Let’s take a look under the hood to see what the Scala compiler actually did to that expression. Compile the file DateUtil.scala using scalac and run javap -c UseDateUtil$, the shadow inner class that Scala created for the singleton, and quickly search for the call to the days method. Let’s examine a few lines of bytecode around the method call:

 
5: invokevirtual #73 // Method
 
DateUtil$.DateHelper:(I)LDateUtil$DateHelper;
 
8: getstatic #69 // Field
 
DateUtil$.MODULE$:LDateUtil$;
 
11: invokevirtual #77 // Method
 
DateUtil$.ago:()Ljava/lang/String;
 
14: invokevirtual #83 // Method
 
DateUtil$DateHelper.days:(Ljava/lang/String;)Ljava/time/LocalDate;

Here’s the cost of the fluent code—an instance of DateHelper is created and then the method days is called on it. Each time you call a fluent method you incur an object creation overhead. The result: The more implicit conversions you make, the more short-lived garbage objects you create.

Scala’s value objects directly address this problem. These little garbage objects are eliminated and the compiler will directly compile the fluent method calls using a combination of functions without an intermediate object. To create a value object, simply extend your class from AnyVal. Let’s modify our DateHelper to a value class.

 
implicit​ ​class​ DateHelper(​val​ offset: ​Int​) ​extends​ ​AnyVal​ {

Extending from AnyVal is the only change we made to DateHelper. The rest of the code in the previous example is unchanged. Now compile the code and take a peek at the bytecode like you did before:

 
8: invokevirtual #78 // Method
 
DateUtil$.DateHelper:(I)I
 
11: getstatic #74 // Field
 
DateUtil$.MODULE$:LDateUtil$;
 
14: invokevirtual #82 // Method
 
DateUtil$.ago:()Ljava/lang/String;
 
17: invokevirtual #86 // Method
 
DateUtil$DateHelper$.days$extension:(ILjava/lang/String;)Ljava/time/LocalDate;

Examine the first lines of the two javap excerpts, one from before the change to DateHelper and the one after. Instead of returning an instance of DateHelper, the compiler has now synthesized the method to return the primitive shown as I for int. Furthermore, the last lines in the two excerpts show that the days method is no longer called on an instance but is written as an extension method. In short, Scala has eliminated the short-lived garbage instance—there’s no longer an instantiation toll to use fluent extension methods.

The implicit wrapper classes like RichInt and RichDouble in Scala are implemented as value classes. While you can write your own implicit classes as value classes, value classes are not limited to this use.

Value classes are useful anywhere a simple value or primitive is adequate but instead you want to use a class for better abstraction. In this case, a value class can give you the best of both worlds: better design and more expressive code without really using an explicit object. Let’s dig into this further with an example.

We can better represent a pet’s name using a class Name than a mere String. Let’s create a Name and use it in a few contexts.

MakingUseOfTypes/NameExample.scala
 
class​ Name(​val​ value: ​String​) {
 
override​ ​def​ toString = value
 
def​ length = value.length
 
}
 
 
object​ UseName ​extends​ App {
 
def​ printName(name: Name) {
 
println(name)
 
}
 
 
val​ name = ​new​ Name(​"Snowy"​)
 
println(name.length)
 
printName(name)
 
}

The class Name has an immutable field named value, a method to return the length of the name, and one to get a string representation. The printName method receives an instance of Name, as we’d expect. The name variable is of type Name. Let’s compile this code and look at relevant parts of the bytecode:

 
5: ldc #76 // String Snowy
 
7: invokespecial #79 // Method
 
Name."<init>":(Ljava/lang/String;)V
 
10: putfield #71 // Field name:LName;
 
13: getstatic #64 // Field
 
scala/Predef$.MODULE$:Lscala/Predef$;
 
16: aload_0
 
17: invokevirtual #81 // Method name:()LName;
 
20: invokevirtual #85 // Method Name.length:()I
 
23: invokestatic #91 // Method
 
scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
 
26: invokevirtual #68 // Method
 
scala/Predef$.println:(Ljava/lang/Object;)V
 
29: aload_0
 
30: aload_0
 
31: invokevirtual #81 // Method name:()LName;
 
34: invokevirtual #93 // Method printName:(LName;)V
 
37: return

There are no surprises here—the compiler creates an instance of Name, invokes the length method on that instance, and passes the instance to printName. Name is a full-blown class. Value classes give us all the benefits of abstraction, but the underlying representation is preserved as a primitive like String, int, and so forth.

By rewriting the Name class as a value class—since it merely wraps a string—we can reap all the benefits of abstraction and preserve it as primitive at the bytecode level. Here’s the change to the Name class.

 
class​ Name(​val​ value: ​String​) ​extends​ ​AnyVal​ {

Now let’s compile and take a peek at the relevant bytecode again:

 
1: ldc #78 // String Snowy
 
3: putfield #75 // Field
 
name:Ljava/lang/String;
 
6: getstatic #64 // Field
 
scala/Predef$.MODULE$:Lscala/Predef$;
 
9: getstatic #83 // Field Name$.MODULE$:LName$;
 
12: aload_0
 
13: invokevirtual #85 // Method
 
name:()Ljava/lang/String;
 
16: invokevirtual #89 // Method
 
Name$.length$extension:(Ljava/lang/String;)I
 
19: invokestatic #95 // Method
 
scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
 
22: invokevirtual #72 // Method
 
scala/Predef$.println:(Ljava/lang/Object;)V
 
25: aload_0
 
26: aload_0
 
27: invokevirtual #85 // Method
 
name:()Ljava/lang/String;
 
30: invokevirtual #97 // Method
 
printName:(Ljava/lang/String;)V
 
33: return

A small change to the source code, but it’s a lot more efficient at the bytecode level. At the source code level printName is still receiving an instance of Name, but at the bytecode level it is taking an instance of String. Likewise the call to the length method changed from a call on an instance to a call of an extension method. Also, the variable name is of type String instead of Name.

Once again, a value class helped eliminate instantiation, but at the same time, it helped create a better abstraction in code.

That was pretty neat, but don’t assume that value classes always avoid instance creation. Scala bends over backward to optimize code and eliminate instantiation; however, there are times when it does create instances for value classes.

Scala will create an instance of value classes if you assign the value to another type or treat it as if it were another type. For example, try adding the following code to the previous example.

 
val​ any : ​Any​ = name

If you compile and examine the bytecode, you’ll notice an instance of Name is created. In order to treat the value as Any, or anything else other than the inherent type of the underlying primitive—String in this example—Scala will create an instance of the value classes. Likewise, if you assign values to an array or make decisions based on its runtime type information, Scala will create instances of the value classes.

Since Scala does not guarantee that objects are not created, it appears as though it will be quite challenging to know for sure if there’s a overhead or not. You may wonder whether you should be peeking at the bytecode each time—but don’t sweat it. Scala creates instances only under these situations and for the most part it will optimize the code. If performance of the code is quite adequate, don’t worry if a particular optimization was done or not. If performance of the code needs improvement, based on real usage and not beliefs, then dig into the bytecode to ensure the optimizations happen where you expect.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.113.80