Elixir — A Tincture for Functional Programming 1.1 What is FP?

Kyle Ledoux
8 min readFeb 1, 2022

Intro

Elixir was unknown to me prior to a few weeks ago when I was made aware that my new team worked with a few microservices that were built with Elixir. Not having any knowledge of or experience with it up until then, I soon realized that I would need to do some learning. My programming experience was entirely in the realm of Object-Oriented Programming, and after dipping my toes into the water with a few basic problems, it became clear that I wasn’t just going to improvise my way through. There are some significant differences with Object-Oriented Programming and honestly, some really great features that are worth knowing.

So, I am writing about Elixir to detail some of the more interesting features of the language and functional programming that I come across as I continue my learning journey with Elixir and Functional Programming. I am not an expert, so always defer to documentation if there is a discrepancy.

I have been working with a few different sources of learning material, all of which are great. Check them out:

Elixir Docs

Learning Functional Programming with Elixir

Joy of Elixir

What is Elixir?

Elixir is a dynamic language that uses functional programming paradigms and leverages the Erlang VM.

Dynamic languages are those in which the types of values are determined at runtime, meaning that the type of a variable does not need to be explicitly stated during compilation. A dynamic program can run with type errors compiled but it will raise an exception during runtime. Static languages on the other hand must have types set prior to runtime and will fail to compile if any type errors are detected.

Let’s see a basic example of dynamic typing in Ruby:

### squid_game.rb# initialize a variable with an implicit type of string
squids_in_tank = "sepia lycidas"
# then try to perform some operation that requires a different type
squids_in_tank -= 1
puts squids_in_tank
###
# run the program
ruby squid_game.rb
#=> NoMethodError on "sepia lycidas" type String

We can run the program, but it errors out at runtime due to typing errors. If we look to a statically typed language like GO, we can try to do the same thing, but because we explicitly declare the type of the variable and the compiler checks its related operations, it will error out during compile time, not run time:

// squid_games.go //
package main
func main() {
var squids_in_tank string = "sepia lycidas"
squids_in_tank -= 1
}
//
go run squid_games.go
//=> Invalid Operation during compile

Functional programming is a programming paradigm; it follows a set of best practices for writing code and designing programs. It differs from Object Oriented Programming in important ways, which we will detail later.

Elixir uses Erlang and the Erlang VM, which is built for scalability and concurrent processing. The Erlang VM uses one Erlang process/thread per core and has a host of other features that make it great for work with parallel processing and distributed systems. This article does a great job at breaking down how the Erlang VM works.

Brewing up a new point of view

A huge part of my early difficulties with Elixir stemmed from my ignorance of functional programming paradigms, so it is really important to answer the question:

What is Functional Programming?

Coming from a world of Object-Oriented Programming, functional programming is a bit of a mental shift. The paradigm carries different views on how to program and what priorities are in program design. There are a few core concepts that are key to making sense of the Functional Programming paradigm.

Core Concept 1: Say WHAT you want, not HOW you want it done!

The first is that the method of programming utilized in Functional Programming is declarative programming, instead of imperative programming. But what does that mean?

In a nutshell, we tell Elixir what we want done, instead of exactly how to do it

Ruby — prepending some text imperatively

# in Ruby we lay out exactly HOW we want the names to be modified
squid_names = ['sepia esculenta', 'common cuttlefish', 'sepia lycidas']
for ind in (0..squid_names.length - 1) do
prefix = "~~~~~~"
new_name = prefix + squid_names[ind]
squid_names[ind] = new_name
end
p squid_names
#=>["~~~~~~sepia esculenta", "~~~~~~common cuttlefish", "~~~~~~sepia lycidas"]

Elixir — prepending some text declaratively

# in Elixir we describe WHAT we want the names to be modified to
defmodule SquidNames do
def names, do: ["sepia esculenta", "common cuttlefish", "sepia lycidas"]
def prefix([]), do: []
def prefix([name | others]) do
padded_length = String.length(name) + 6
[String.pad_leading(name, padded_length, "~") | prefix(others)]
end
end
IO.inspect(SquidNames.prefix(SquidNames.names))
#=>["~~~~~~sepia esculenta", "~~~~~~common cuttlefish", "~~~~~~sepia lycidas"]

With Elixir we tend to avoid dense code blocks that explicitly lay out how we want to do something step by step in favor of succinct, reusable functions that describes what we want done. We just say what we want and let the compiler figure out the best way to do it. Typically, we make use of recursion to do so, but I’ll get into that in greater detail later.

Declarative languages serve as an abstraction layer for detailing exactly how we want to obtain some result. This allows the compiler to determine how to handle operations under the hood, which frequently results in more efficient code than we might write on our own imperatively.

Core Concept 2: Functions are everything

Functions are the basic building blocks that we use for constructing our programs. This may seem at first glance to be the same as any other paradigm, but with Functional Programming we really mean it. Everything is built with functions. With Elixir (and Functional Programming) we generally eschew control flow mechanisms like `case` and `switch` statements and `if/else` blocks, for function clauses (think “miniature” functions that handle function calls with specific sets of arguments. More on these later).

Essentially, many smaller functions are combined to build larger more complex programs.

This might seem pretty similar to a familiar Object-Oriented Programming approach in which we define methods for classes and use them as needed to operate on the data of the instance, but this is where there is a very important divergence. With Functional Programming, the data on which we are operating and the operation itself are separated. The function simply acts on data that it takes as an argument and returns a new value. That’s it.

In Object-Oriented Programming, data is often stored in objects as state, and the state (read: data) that is being operated on can impact the method itself. This is because the methods belong to that object; there is a dependency between them.

In this Ruby class, we can see how the push function belongs to the MySquids object, and its functionality is dependent on the state of the squid_names collection therein:

class MySquids
attr_reader squid_names
def initialize()
@squid_names = []
end
def push(name)
# the functionality of this method depends on the state #
# of the @squid_names collection #
squid_names.push(name) unless names.include?(name)
end
endsquids = MySquids.new
squids.push("inky")
new_squids = MySquids.new
new_squids.push("blinky")
squids.push("inky")
# => ["inky"]
new_squids.push("inky")
# => ["blinky", "inky"]

In Elixir however, we can see that while we can create a collection using a struct of the SquidNames module, the push method defined therein is not impacted by the state of the struct. It simply takes an argument (not just the struct collection) and operates independently.

defmodule SquidNames do
defstruct names: []
def push(set = %{names: names}, name) do
if Enum.member?(names, name) do
set
else
%{set | names: names ++ [name]}
end
end
end
set = %SquidNames{}
set = SquidNames.push(set, "inky")
new_set = %SquidNames{}
new_set = SquidNames.push(new_set, "blinky")
IO.inspect SquidNames.push(set, "inky")
# => ["inky"]
IO.inspect SquidNames.push(new_set, "inky")
# => ["blinky", "inky"]

Example with ruby and elixir: ruby class with state for tracking squid names, method to add a squid only if squid name doesn’t exist (not in collection) ; elixir copy module from book with struct

Functions in Functional Programming serve a simple but critical role:

  1. Take data
  2. Perform some operation
  3. Return some values

Core Concept 3: Bird Immutability’s the Word

Functional programming works with immutable values. Immutable values cannot be modified after their creation.

Using immutable values helps us maintain a separation between the data and data operations. When values are immutable, we know that the operation is not attached to any data other than whatever is passed in as the current argument (ideally).

Take a look at this collection in Ruby. We can push a new element into the collection, and the original object is mutated:

squids = ['inky', 'blinky']squids.push('pinky')p squids
#=> ['inky', 'blinky', 'pinky']

On the other hand, when we push a new node into a list in Elixir, a new list is returned and the original is not mutated:

squids = ["inky", "blinky"]squids ++ ['pinky']IO.inspect(squids)
#=> ["inky", "blinky"]
# squids is immutable, no changes were made

One of the main advantages that Elixir has over imperative languages, which use mutable values, is that all values in Elixir are immutable, which makes leveraging concurrency and multiple cores much easier, with less room for error/confusion.

Let’s look at a contrived example to illustrate this. Suppose we have two operations that are being performed concurrently on the same mutable data source; it is possible to introduce bugs that are very difficult to trap resulting directly from the mutability of the data.

nums = [ 1, -3]# Thread 1: Take collection and find sum by popping values from collection
# Thread 2: Take collection and reverse sign of all negative numbers
# We would expect that if the signs of negative numbers are reversed and the sum calcullated, the sum would be 4...# Thread 1 -> Step 1
sum == 0
nums == [ 1, -3]
sum += nums.pop
nums == [ 1 ]
sum == -3
# Thread 2-> Step 1
current_value == 1
current_index == 0
nums == [ 1, -3]
# Positive int no need to flip
# Thread 1 -> Step 2
sum == -3
nums == [ 1 ]
sum += nums.pop
sum == -2
nums == []
# Thread 2-> Step 2
current_value == nil
current_index == 1
nums == []
# No element at index

Both operations point to the same data source, and because it is mutable, they act on it simultaneously, with one operation affecting the expected output of the other.

With Elixir, there is no mutability of values so operations can be performed on the same data in parallel without each affecting the outcome of the other.

Wrapping Up

There is a lot to learn about Elixir and Functional Programming, and we’re just getting started. Here are the key take-aways for this introduction:

  • Elixir is a dynamically typed language
  • Elixir operates under the Functional Programming paradigm
  • Functional Programming relies on Functions instead of Objects
  • Functional Programming makes use of Declarative Programming to abstract away imperative implementation details
  • Functions in Elixir do three things: accept data, operate on the data, return some values
  • Values in Elixir are immutable

More exciting coverage of Elixir’s facets and fun to come!

--

--

Kyle Ledoux

I’m a software engineer with a talent for distractable curiosity and a passion for questionable humor.