Scala: Learn to walk before you fly (Part 2)
Part 2 – Building immutability
Welcome back to Part 2! In Part 1 we saw Scala as a language that facilitates and drives you towards the functional world. Functional programming has a lot of advantages, but working really well with immutability is for sure my favorite one.
At the end of Part 1, I mentioned that we need a good toolkit with
.fold that could help us in the immutable world. I was going to write about them in Part 2, but on second thought, I think it is for the best to devote some time to Building Immutability first. We’ll tackle an Immutability Toolbox soon enough in an upcoming post.
I think I’ve mentioned it a couple of times already, but Immutability is great! Let’s briefly walk through some of its core points.
- It is thread-safe
- No one can change it, so you don’t need to keep track of what other methods/flows may do to your variables
- It’s less error-prone and easier to debug since the value of a variable is initialized with its declaration
Now that we are all on the same page, let’s take a look at some of Scala’s syntax, and see how opinionated it is towards Immutability.
Var vs Val
One thing to keep in mind is that you should always use a val unless you have a really good reason to use a var. This should be your default mindset, and linters like Scalastyle will even flag it as a problem if you do use var on your code.
That said, var is a normal variable that you would declare in Java, where the val is a final variable.
int age = 23; age += 10; final int immutableAge = 23; //Does not compile immutableAge += 10;
var age: Int = 23 age += 10 val immutableAge: Int = 23 //Does not compile immutableAge += 10
In Scala, you can write val instead of final int, which looks so much better! Let’s be honest, people will either be lazy or forget about the final most of the time, whereas val is a different keyword, not an extra one. That makes a huge difference.
On a related note, talking about laziness, there are a lot of times where the type of a variable is obvious. That is why Local Type Inference is a great mechanism to have. While Scala and C# have supported it for many years, Java 10 is finally catching up, and as of last year, you can also use var in Java!
//This code is valid for both Java 10 and Scala var age = 23;
(While ending statements with a semicolon is valid syntax in Scala, it is not common practice.)
Remember: just like final, val and var only reflect the immutability of the declared variable. That is not enough for objects. Even if the reference is immutable, that does not make the data immutable. This is important to understand when you are dealing with objects or primitive types like int.
Scala is actually a bit different than Java regarding what is seen as a primitive type or an object. In Scala there is Any and AnyVal, which you don’t need to worry too much about – but it is relevant to know that the Scala Int behaves like Java int because it extends from AnyVal. It is also worth mentioning that there is AnyRef that corresponds to java.lang.Object, and both AnyRef and AnyVal extend from Any. Feel free to check out Scala Type Hierarchy for more about this.
Immutable data structures
When you create a List as val (or add a final in Java), only the reference to the list is immutable. That means that you cannot change it to reference another list; however, you can add or change its contents.
final var names = new ArrayList<>(); names.add("Bob"); names.add("Alice"); //Does not compile - Cannot assign a value to final variable names = new ArrayList<>();
val names = new scala.collection.mutable.MutableList[String] //Note that the += is a normal method like java .add, using Infix Notation names += "Bob" //Legal, but definitely strange names.+=("Alice") //Does not compile - Reassignment to val names = new scala.collection.mutable.MutableList[String]
In one of these examples, we use Infix Notation, which you can read more about at the official docs.
Both examples have a Mutable List, but note that in Java, that is the standard way to do things. If you want to go immutable in Java, you’ll need alternatives.
On the other hand, in Scala, you need to explicitly say MutableList because by default Scala does not want you to use this.
In Scala, you have both immutable and mutable collections, but as expected, Scala is opinionated towards immutability. To know more about what kind of collections exist on the standard library, you can take a look at this Collections overview.
So, how do you make an Immutable List in Scala?
val names = List("Bob", "Alice") val namesWithAlbert = names :+ "Albert" //Note that the += method of MutableList no longer exists for List //This += is actually 'names = names + "Jack"' that does not work for Lists names += "Jack"
Could it be any simpler? You don’t even need to write List[String] thanks to Type inference. The names list is immutable, so if we want to add elements to it, we cannot. The :+ method actually creates a new list with the previous names and adds Albert to it.
Creating a new list every time I add an element??
This may sound pretty inefficient at first, and actually :+ (appending to the end of the list) is a really bad idea. So let’s make a quick change that will make it much better.
val names = List("Bob", "Alice") val namesWithAlbert = "Albert" :: names
Now we are using :: (prepend on the list), which is a colossal difference. This is the difference between O(n) or O(1), meaning that the first approach will grow linearly with the size of the list, whereas the prepend will take constant time.
The important thing to notice is that what allows for the prepend on immutable lists to be efficient is the fact that the original list cannot change. Lists implementation relies on each element pointing to the next one, so we can see names as a pointer to “Bob” (and then “Bob” points to “Alice”), and namesWithAlbert as a pointer to “Albert”. What this means is that we don’t really need to make a copy of names because it is Immutable – namesWithAlbert can rely on *names* to hold the rest of the list.
Why did I even start with the :+ then? First of all, it is important to show things to avoid, but in all honesty, I wanted to stay away from a corner case on Scala naming of methods. Since it’s too late now, let’s take a quick detour into naming rules.
obscure simple rule
You can read the full quote below, but long story short, “If the method name ends in a colon, the method is invoked on the right operand”. Meaning that:
//The method actually exists on the List, this is a valid way to invoke the method val namesWithAlbert = names.::("Albert") //Best way to use this method, taking advantage of Infix Notation val namesWithAlbert = "Albert" :: names
Quoting Programming in Scala, Third Edition:
“In the expression “1 :: twoThree”, :: is a method of its right operand, the list, twoThree. You might suspect there’s something amiss with the associativity of the :: method, but it is actually a simple rule to remember: If a method is used in operator notation, such as a * b, the method is invoked on the left operand, as in a.*(b) — unless the method name ends in a colon. If the method name ends in a colon, the method is invoked on the right operand. Therefore, in 1 :: twoThree, the :: method is invoked on twoThree, passing in 1, like this: twoThree.::(1). Operator associativity will be described in more detail in Section 5.9.”
To be honest, I still remember feeling incredibly incredulous the first time I read this, but let’s call it a creative decision and move along.
Speaking of interesting naming decisions, there is one that you must really know about: .apply()
You may have noticed that we didn’t use the new keyword to create a list. Scala, just like Java, uses new to create a new instance (like we did in the MutableList example) so what is really happening there?
//Calling the apply method on List val names = List.apply("Bob", "Alice") //Syntactic sugar that compiles to List.apply("Bob", "Alice") val names = List("Bob", "Alice")
Basically, apply is a special method name that will be called when no method name is provided. In Scala we take advantage of it in some interesting ways; for instance, we could apply the Factory Pattern. We will get back to this on the next blog post.
Any more special naming rules?
I hope I am not scaring you off with those special method names. It may sound a bit too creative at first glance, but there aren’t any more surprises regarding special method names, so it is not a lot of magic to keep track of, for now.
We’ll see more of the apply, and what is happening with those class methods like apply. In Scala, we don’t have access to static methods — which is actually a very good thing — so there must be a way to define those. More importantly, we will look at two amazing constructs, case class, and object. They are simple, elegant and make so much sense, and you will love them!
Like always, if you have any suggestions, feel free to ping me on Twitter @machadoit.
See you in Part 3!
About The Author: João Machado is a Software Engineer Lead at Codacy. He has worked at Codacy for 4 years. In his free time, he aims for simplicity and is a board game geek.