Embrace immutability (Programming & Infrastructure)
Jorge Tovar
Posted on September 21, 2022
The future of Software programming is immutable code.
Mutability 🚨
If you are a programmer, and you have some experience with Languages like Java, Python, and Go likely make you aware of the benefits and drawbacks of mutability. The problems associated with mutability are numerous, including:
Increasing Complexity: Which function changed the value? Why is this property null?
Concurrency: Multiple threads modifying the same variable simultaneously
With immutable data structures, it's possible to share data without concerns between threads and functions. Even in the infrastructure world, the norm used to involve deploying servers and constantly updating and modifying them in place. Consequently, it became challenging to keep track of all the changes and create new environments from scratch.
Immutable infrastructure represents another paradigm shift in infrastructure where servers are never modified. If an update is required, new servers are built and provisioned to replace the old ones.
In the book 'Effective Java,' the author advocates using the final keyword in methods and classes. Why? The final keyword is a non-access modifier used for classes, attributes, and methods, which renders them non-changeable (impossible to inherit or override). It proves useful when you want a variable to always store the same value, such as PI.
Languages like Java, Go, and Python default to having mutable objects, making it much harder to reason about the problem at hand. The same scenario applies in the infrastructure world, with configuration management tools like Chef, Puppet, and Ansible that typically default to a mutable infrastructure paradigm.
Immutability comes to the rescue, inspired by functional programming, where variables are immutable. Once you set a variable to a value, it can never be changed. If an update is needed, a new variable is created.
When values don't change, it’s easier to reason about your code.
The good news is that is possible to overcome this problem! 😎
Go Pointers
- Pass values instead of references
package main
import (
"fmt"
"math/rand"
"strconv"
"strings"
)
type Book struct {
Author string
Title string
Pages int
}
func (book Book) GetPagesWithCode() string {
var n = rand.Intn(200)
book.Pages -= n
return "The " + strings.ToUpper(book.Title) + " book has " + strconv.Itoa(book.Pages) + " pages"
}
func main() {
var book = Book{Author: "Rich Hickey", Title: "Clojure for the Brave & True", Pages: 232}
fmt.Printf("1) %+v\n", book)
changeAuthor(&book, "Daniel Higginbotham")
fmt.Printf("2) %+v\n", book)
fmt.Println(book.GetPagesWithCode())
fmt.Printf("3) %+v\n", book)
}
func changeAuthor(book *Book, author string) {
book.Author = author
}
Output:
Even when we updated the pages in the method, the data remain the same.
1) {Author:Rich Hickey Title:Clojure for the Brave & True Pages:232}
2) {Author:Daniel Higginbotham Title:Clojure for the Brave & True Pages:232}
The CLOJURE FOR THE BRAVE & TRUE book has 151 pages
3) {Author:Daniel Higginbotham Title:Clojure for the Brave & True Pages:232}
Clojure
- Create functions that return new values
(ns main.core
(:require [clojure.string :as s])
(:gen-class)
)
(defn get-pages-with-code [book]
(str "The " (s/upper-case (:title book)) " book has " (:pages book) " pages")
)
(defn change-author [book author]
(assoc book :author author))
(defn -main
[& args]
(let [book {:author "Rich Hickey", :title "Clojure for the Brave & True", :pages 232}
new-book (change-author book "Daniel Higginbotham")]
(println "1)" book)
(println "2)" new-book)
(println "3)" (get-pages-with-code book))
)
)
Output:
The data is never mutated, we just create new values
1) {:author Rich Hickey, :title Clojure for the Brave & True, :pages 232}
2) {:author Daniel Higginbotham, :title Clojure for the Brave & True, :pages 232}
3) The CLOJURE FOR THE BRAVE & TRUE book has 232 pages
Terraform
- Deploy new servers instead of updating current ones
resource "aws_instance" "george_server" {
ami = "ami-0fb653ca2d3203ac1"
instance_type = "t2.micro"
}
A completely new server
resource "aws_instance" "george_server" {
ami = "ami-0CCC3ca2d32CCC1"
instance_type = "t2.micro"
}
Because servers never change, it’s a lot easier to reason about what’s deployed.
Conclusion
Controlling which parts of your program can make changes to variables and objects will allow you to write more robust and predictable software.
Embrace immutability 🔥
severity = :mild
error_message = "OH GOD! IT'S A DISASTER! WE'RE "
if severity == :mild
error_message = error_message + "MILDLY INCONVENIENCED!"
else
error_message = error_message + "DOOOOOOOMED!"
end
You might be tempted to do something similar in Clojure:
(def severity :mild)
(def error-message "OH GOD! IT'S A DISASTER! WE'RE ")
(if (= severity :mild)
(def error-message (str error-message "MILDLY INCONVENIENCED!"))
(def error-message (str error-message "DOOOOOOOMED!")))
Posted on September 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.