People are always asking about the benefits of immutability and why it’s a big deal for the Scala community. I learned about the copy-and-swap pattern from C++ before I learnt Scala and it’s why I’m grateful Scala won’t let me shoot myself in the foot.
Let’s look at a typical piece of Scala code from the Object-Oriented design world that embraces private mutable variables, believing that hiding state simplifies the design of the application:
class Person(name :String, phoneNumber :String = "") {
private var mName :String = ""
private var mPhoneNumber :String = ""
def getName() :String = mName
def setName(name: String): Unit = {
mName = name
}
def getPhoneNumber() :String = mPhoneNumber
def setPhoneNumber(phoneNumber: String): Unit = {
if (phoneNumber.length < 8)
throw new Exception("bad phone number")
else
mPhoneNumber = phoneNumber
}
setName(name)
setPhoneNumber(phoneNumber)
}
val emergencyContact = new Person("robert", "9326489432")
println(s"emergencyContact name: ${emergencyContact.getName()}")
println(s"emergencyContact phone number: ${emergencyContact.getPhoneNumber()}")
This class maintains a name and a phone number, providing a getter and setter for each. Let’s look at how it might be used inside an application
def updateEmergencyContact(newName :String, newPhone :String) = {
emergencyContact.setName(newName)
emergencyContact.setPhoneNumber(newPhone)
}
updateEmergencyContact("mark", "7384")
When you run the code above it explodes because the phone number isn’t 8 characters long. You can capture the exception but you cannot handle it gracefully.
scala> println(s"emergencyContact name: ${emergencyContact.getName()}")
emergencyContact name: mark
scala> println(s"emergencyContact phone number: ${emergencyContact.getPhoneNumber()}")
emergencyContact phone number: 9326489432
What’s happened here is that the setName
change worked, so now it says “mark” but the phone number wasn’t updated, it’s still robert’s phone number. In an emergency you’d phone Robert and ask for Mark, and Robert responds, sorry bro, wrong number.
This class of error is deeper than just poor error handling. The internal state of the application is corrupted. If you caught the bad phone number exception .. you still can’t recover from the error without a deep internal knowledge of the order to setXXX functions and what the state was before.
In C++ if a variable called a
is the same type as a variable b
then a = b
; cannot fail. It is also an atomic operation – it happens instantly or it doesn’t. All you are doing when you say a = b
is updating a pointer.
If your program works like this…
- Copy the initial state (var `
b = a.copy
) - Modify b however you want:
b.setName(...)
- Overwrite the reference at the end:
a = b
..then your application will always maintain a valid state. If it crashes before the third line, a
stays exactly how it was, which is at least consistent. If the final line executes, it’s also consistent. Let’s look at using wholistic objects in our approach:
val newPerson = new Person(newName, newPhoneNumber) // explodes
emergencyContact = newPerson // if reached, cannot fail
C++ uses copy constructors like many other object oriented languages do. In Scala we also have the benefits of using case classes which give us a copy
function for free.
By designing the way you update data in your application to be around “whole classes and copying things” you simplify the error handling for your application. Extending the use of immutability everywhere allows you to trust the application more. You could draw comparisons between “copy and swap” with how databases implement transactions that are either committed or rolled back.
You may consider this a case of bad programming and not handling a validation issue correctly, you presumably don’t make this example mistake in your own code, but network/file exceptions are common and unavoidable. This pattern is more about graceful failure, retaining consistency easily being able to continue when things do go wrong. It works well on larger object hierarchies where you are scanning and updating multiple things at once.
Applying immutability everywhere, or using libraries like cats effect to help eliminate side effects are natural extensions to this principle. It may take some time to fully warm to however.