Book Review: Introducing Elixir by Simon St. Laurent, J. David Eisenberg
Book Bit: Bits and Pieces of Books, Reviews Relevant to Our Age
Link to Book on Publisher’s Website
Overall Summary
While it’s tempting to just go ahead and read a programming book unreflectively and set it aside once you’ve gotten what you wanted out of it, I thought it would be a good idea to actively try to think through what I’m doing here.
Yes, it’s an introductory Elixir book, but a couple points I would make is that I believe it was written in an incredibly clear manner, without forcing the user to make too many assumptions.
Key Points
It’s a general map of Elixir from a getting-started standpoint.
Here’s a high-level overview of what gets covered:
Functions and Modules
Atoms, Tuples, and Pattern Matching
Logic and Recursion
Communicating with Humans
Lists
Name-Value Pairs
Higher-Order Functions and List Comprehensions
Playing with Processes
Exceptions, Errors, and Debugging
Static Analysis, Typespecs, and Testing
Storing Structured Data
Getting Started with OTP
Using Macros to Extend Elixir
My Editorialization as a Reader
Functional programming is said to be, “difficult to comprehend,” for many people. My interests in reading this book was to get started and try to figure out what all the fuss is about. I had previously dabbled a bit in Haskell, which is said to be the quintessential functional programming language, but I found myself to feel fairly unmotivated as the more I read about Haskell’s use cases, the narrower the scope and more esoteric it seemed. Elixir on the other hand, is kind of exciting as a functional program which wraps around this mysterious, “Erlang,” language created by Ericcson in the 1980s and 1990s. This paper outlines some of the original objectives behind Erlang:
The initial development of Erlang took place in 1986 at the Ericsson Computer Science Laboratory (the Lab). Erlang was designed with a specific objective in mind: “to provide a better way of programming telephony applications.” At the time telephony applications were atypical of the kind of problems that conventional programming languages were designed to solve. Telephony applications are by their nature highly concurrent: a single switch must handle tens or hundreds of thousands of simultaneous transactions. Such transactions are intrinsically distributed and the software is expected to be highly fault-tolerant. When the software that controls telephones fails, newspapers write about it, something which does not happen when a typical desktop application fails. Telephony software must also be changed “on the fly,” that is, without loss of service occurring in the application as code upgrade operations occur. Telephony software must also operate in the “soft real-time” domain, with stringent timing requirements for some operations, but with a more relaxed view of timing for other classes of operation.
Basically, the telephone was the precursor to the Internet, and downtimes in telephone networks of yesteryear were treated like downtimes in internet services today. This suggests that Elixir, being a wrapper for Erlang, may perhaps be a way to help solve devops problems, in that if the language itself provides some sort of inherent fault tolerance, that may reduce the need for devops resources.
As far as I can tell today scalability is solved with things like Kubernetes, which essentially means scaling up compute resources as usage (e.g. the number of visitors to a site) increases. In short, the way to solve problems is to simply, “throw more hardware at it.” This solution simply did not exist in the 1980’s, as hardware was basically fixed, which may preclude the need to learn Elixir, and yet it remains an intriguing language as of course the idea of actually being able to preclude the use of Kubernetes, which in itself has a high learning curve. There are debates online about whether Elixir has something to learn from Kubernetes.
I can’t say that I have a definitive answer for any of these questions within the scope of this book review, but I can give some take-aways from the standpoint of just getting started wrapping my head around functional programming, within the context of Elixir:
There’s this concept that keeps getting thrown around called, “data immutability,” but that doesn’t get expanded upon much.
It’s a bit confusing because when you do something like create an int A=1 and then want to change it to A=2 this, this is called, “variable reassignment.” However, there does not appear to be anything special here, it seems to be the same as any other, non-functional programming language, where you change the value of a variable, right?
Wrong — because that would mean that data is mutable…remember, data is immutable, it cannot be changed. In Elixir, the data structures themselves (like maps) are immutable. Once a map is created, it cannot be changed. When we say:
map = %{"key1" => "value1"}
map = Map.put(map, "key2", "value2")
What's actually happening is that
Map.put
is creating a new map that includes everything from the originalmap
plus the new key-value pair. Then we're reassigning the variablemap
to this new map. The original map (%{"key1" => "value1"}
) is unchanged by this operation.When we say a data structure is "immutable," it means that once it's created, it cannot be changed. Any operation that seems to change it is actually creating a new data structure. So when we do the following in Python:
a = 1 a = 2
You are reassigning the variable
a
from1
to2
, just like in the Elixir example. And just like in Elixir, the integer1
still exists, buta
no longer refers to it. I know this sounds confusing.The difference between Elixir (and other functional languages) and Python (and other imperative languages) comes more into play with composite data structures, like lists or dictionaries in Python, or lists or maps in Elixir.
In Python, these composite data structures are mutable, meaning you can change their content without creating a new object. For example, if you have a list
lst = [1, 2, 3]
in Python, you can change it to[1, 2, 4]
by doinglst[2] = 4
. After this operation,lst
is still the same list object as before (i.e., it's in the same location in memory), but its content has changed.In Elixir, on the other hand, composite data structures are also immutable, just like simple values. If you have a list
lst = [1, 2, 3]
in Elixir and you want to change it to[1, 2, 4]
, you'd need to create a new list. The original list remains[1, 2, 3]
.So why would one want to use variable reassignment rather than mutable variables? What we get told is it’s because of predictability. The benefit of immutability comes into play when you have multiple parts of your code that are referring to the same data. If the data is mutable and one part of the code changes it, then all other parts of the code that are referring to the same data will also see that change. This can lead to bugs that are hard to track down because the state of your data can change unexpectedly.
Here's an example in Python to illustrate this:
# Let's say we have a list original_list = [1, 2, 3] # And we pass this list to a function def add_value(lst, value): lst.append(value) add_value(original_list, 4) # Now, original_list has changed print(original_list) # prints: [1, 2, 3, 4]
In this example,
original_list
was changed by theadd_value
function. If another part of your code was expectingoriginal_list
to remain as[1, 2, 3]
, that could lead to a bug. This kind of bug can be difficult to debug because the change tooriginal_list
happened "behind the scenes" (meaning, the human doesn’t immediately know that this happened simply from reading the code) from the perspective of the code that's usingoriginal_list
.
Now let's compare to how this would work in Elixir:
# Let's say we have a list original_list = [1, 2, 3] # And we pass this list to a function def add_value(lst, value) do lst ++ [value] end new_list = add_value(original_list, 4) # Now, new_list is [1, 2, 3, 4], but original_list is still [1, 2, 3] IO.inspect(original_list) # prints: [1, 2, 3] IO.inspect(new_list) # prints: [1, 2, 3, 4]
In this Elixir example,
original_list
is not changed by theadd_value
function. Instead,add_value
returns a new list, andoriginal_list
remains the same, e.g. the actual variable and therefore the data in the variable itself does not get, “destroyed,” as it would in the python example. This makes the code more predictable, because you know that once you've createdoriginal_list
, it won't change unless you explicitly reassign it. This can make your code easier to reason about and reduce the chance of bugs.
The key difference is that in the Python example, the
add_value
function was able to changeoriginal_list
"behind the scenes". In the Elixir example, the only way to change whatlist_a
refers to is with an explicit reassignment. There's no way for a function to changelist_a
without us knowing about it. This makes the code more predictable. But what do I mean by, “behind the scenes?” Is there something fancy going on within the system level? No, "behind the scenes," in this context simply has to do with code readability and understanding how data is being changed in the program. It's about how clearly and explicitly the code signals changes to data.So that’s it basically, it purely has to do with how a person writing the code is able to more cleanly and clearly read how data is being changed in the program. So how do we get around the obvious problem that this presents in that we would seemingly need a huge number of variable names?
One way is the Pipe operator: Elixir's pipe operator (
|>
) allows you to chain operations together. This reduces the need for temporary variable names because the result of each operation is immediately passed to the next, similar to how it is done in bash.map = %{"key1" => "value1"} |> Map.put("key2", "value2")
Small functions: In functional programming, it's common to write small, single-purpose functions. This helps to keep any one function from getting too complex, reduces the number of variables you need to keep track of in any one context, and promotes code reusability.
So overall, you call it functional programming because it uses functions to get around changing data and structures. Here’s an article written in the context of Haskell which gets toward what’s going on in a visual way, and here’s the illustration they use:
Basically the data in the original container, the purple dot, remains the same, it doesn’t change, nor does the container, but there’s a, “new thing,” that gets created, another container with a red dot in it. The function doesn’t change the purple dot itself, it creates a new thing.
How does this translate back to the original problem set of designing a telephony-friendly language which is fault tolerant? What I imagine is that it meant that ideally, as a functional programming language, developers could more easily visualize or prevent problems before they occurred by not being able to just change data on the fly, and being forced to read and think through how to be careful with these, “dots,” as you never know if something else may dependent upon that dot you are about to change.
This is a separate design consideration from some of the other features of Erlang, e.g. concurrency.