(examples can be found on GitHub)
The first point is using Immutability as Default.
What does "as default" mean? We are not barring mutability from applications wholesale; there are practical reasons for using mutability. For instance,
- performance
- global settings.
However, the mutable code should be bounded by its defining scope. This idea can be captured with four rules of thumb:
- Function inputs are immutable.
- Function outputs are immutable.
- var and collection.mutable values are local and temporary.
- Function return values are placed into a val.
OO Imperative Style
First, we have a helper function for our examples.
def color(str: String): String = { str match{ case "One Fish" => "Red Fish" case "Two Fish" => "Blue Fish" } }
This snippet defines an imperative style One Fish Two Fish Red Fish Blue Fish. Note that we have two valid data points
- ("One Fish", "Red Fish")
- ("Two Fish", "Blue Fish")
Yet, we model it with a function from String to String which is a space much larger than 2 points. Also, it throws an exception on bad input; the function has a return type of String which is a lie since a thrown exception is a very different result from a String. I found code like this to be very common in the OO imperative world. In the coming posts we will seek to replace this with something better.
Some typical code which breaks our general rules looks a little like:
def bad(): mutable.Buffer[String] = { val fish = mutable.Buffer[String]() var one = "One Fish" var two = "Two Fish" fish.append(one) fish.append(two) one = color(one) two = color(two) fish.append(one) fish.append(two) fish }
Here, mutable inputs or outputs can very easily poison threaded data in an application. This makes it difficult to follow the data through an application and reason about control flow.
Also, the vars one and two are used to first hold constant data then hold the result of a function call. The functional assignment to a var has similar negative cognitive affects as mutable arguments and return values. They needlessly complicate control flow.
Typelevel Style
The typelevel way to write the same functionality would be close to:
def better(): List[String] = { val one = "One Fish" val two = "Two Fish" List( one, two, color(one), color(two) ) }
The function is self contained. No mutable state inside the function escapes its defining scope and all function calls are nested readably within other function calls. The important part is the intent of the function is clear. Also, any threading performed around it is safe from accidental data poisoning; no messy synchronization calls are necessary.
Now that we have a foundation of immutability, we'll add combinators to our toolset.