Functional Programming Properties of Erlang and Elixir

Describing different properties of Erlang and Elixir from a functional programming perspective.

Erlang and Elixir are functional programming languages and there are important differences in approaches to solving problems versus imperative programming languages.

We often hear about immutability and various properties of languages in the functional paradigm, but let's pull on these threads a little to describe different aspects of Erlang and Elixir from a functional programming perspective.

💡
This isn't intended to be a comprehensive deep-dive into different aspects of various languages, but rather a brief overview describing and classifying properties of Erlang and Elixir.

Compiled vs Interpreted

First, Elixir programs are compiled into bytecode and run on the Erlang Virtual Machine or BEAM.

In a compiled language, the target machine directly translates the program. In an interpreted language, the source code is not directly translated by the target machine. Instead, a different program, aka the interpreter, reads and executes the code.

Programs that are compiled into native machine code tend to be faster than interpreted code, because the process of translating / interpreting code at run-time adds to the overhead, and can cause the program to be slower overall.

Pure vs Impure

Erlang and Elixir are impure functional languages simply because side-effects can be produced while evaluating an expression (spawning a task, message passing between processes, updating an ETS table, etc).

Haskell on the other hand is considered a pure functional language because it tracks side-effects in it's type system.

💡
The "pure" in the "pure functional" is a property called "referential transparency".

Static vs Dynamic Typing

Elixir is dynamically typed which means that type checking is performed at runtime, while statically typed languages perform type checking at compile time.

Programs written in dynamically-typed languages can compile even if they contain errors that will prevent the application from running properly (if at all). If a program written in a statically-typed language (such as Rust) contains errors, it will fail to compile until the errors have been addressed.

Second, statically-typed languages require data types of variables to be declared before being used, while dynamically-typed languages do not.

Consider the two following code examples:

  1. Rust Example:
let guess: u32 = 42
Rust example from the Rust Book.

2. Elixir Example:

guess = 42

In the Rust example, the type of the value assigned to guess is explicitly defined as an unsigned, 32-bit integer.  This enforces checks at compile-time for the value assigned to guess:

  1. the value must be a positive integer (unsigned)
  2. the value must be 32-bits or 0 to 232 - 1
💡
Note: Dynamic typing was historically chosen for simple reasons; those who implemented Erlang at first mostly came from dynamically typed languages, and as such, having Erlang dynamic was the most natural option to them.

Strong vs Weak Typing

Strong typing is a phrase with no widely agreed upon meaning (it's often conflated to mean static typing).  Whenever a language is described as being strongly or weakly typed, the inference is usually whether or not a language does "implicit conversion".  

For instance, if you attempt to add an integer and a string, an exception will be thrown for bad arguments:

iex> n = 5 + "1"
** (ArithmeticError) bad argument in arithmetic expression: 5 + "1"
    :erlang.+(5, "1")
    iex:1: (file)
Elixir strong-typing

With these definitions, Javascript could be considered a weakly typed language:

n = 5 + "1"
"51"
Javascript weak-typing

Generally, a strongly typed language has stricter typing rules at compile time, which implies that errors and exceptions are more likely to happen during compilation.

A weakly typed language has looser typing rules and may produce unpredictable or even erroneous results or may perform implicit type conversion at runtime.

Type System Trade-Offs

Briefly, without rabbit-holing into the complex conversation around type systems, there are some trade-offs when working with static vs dynamically typed languages.

Here's a visual representation of the few languages we've mentioned:

  • It's suggested that static languages can enforce correctness and reduce unexpected runtime errors, but sometimes at the cost of more overhead, more verbose code and less productivity.
  • Erlang's (and therefore Elixir's) philosophy is to assume that errors will happen anyway and to ensure that these cases are covered and that the dynamic type system is not a barrier to reliability and safety of programs.

There have also been advances in static type analysis for Erlang via Dialyzer and other tools and attempts to add an official Type-System over the years.  

Invariably, these attempts surfaced issues dealing with Erlang's declarative, expressive syntax for pattern matching and adding types for Processes / Message Passing which are some of the language's greatest strengths.

💡
Details about attempt to add Type Systems to Erlang including the reports can be found here: https://learnyousomeerlang.com/types-or-lack-thereof

Declarative vs Imperative

Imperative code focuses on writing an explicit sequence of commands to describe how you want the computer to do things, and declarative code focuses on specifying the result of what you want.

Functional programming is a paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm in which function definitions are trees of expressions that each return a value, rather than a sequence of imperative statements which change the state of the program.

Imperative Example

var list = ["dogs", "hot dogs", "bananas"];
function upcase(list) {
  var newList = [];
  for (var i = 0; i < list.length; i++) {
    newList.push(list[i].toUpperCase());
  }
  return newList; 
}

console.log(upcase(list))
//Output => ["DOGS", "HOT DOGS", "BANANAS"]
Javascript - Imperative Example

In the imperative setting, it is necessary to use control flow structures like for to navigate through each element of the list, incrementing the variable i. Then, we need to push the new upper-cased string in the newList variable.  The details of problem can be obfuscated by boilerplate actions and mutating values.

Declarative Example

defmodule StringList do
  def upcase([]), do: []
  def upcase([first | rest]), do: [String.upcase(first) | upcase(rest)]
end

IO.inspect StringList.upcase(["dogs", "hot dogs", "bananas"]) 
# => ["DOGS", "HOT DOGS", "BANANAS"]

The declarative version in Elixir focuses on what is necessary which can be made even simpler by using built-in functions:

list = ["dogs", "hot dogs", "bananas"] 
Enum.map(list, &String.upcase/1)
# => ["DOGS", "HOT DOGS", "BANANAS"]
💡
The Javascript example above is mutating the value of newList and i as it iterates over the list which is a great segue into the next topic: Immutability.

Immutability vs Mutability

In Elixir and most functional programming languages, all values created in the program are immutable, meaning that each function will have a stable value.

Imperative languages use mutating, shared values that can be destroyed and to account for this, most require thread and lock mechanisms to work with concurrency and parallelism.  

Updating a functional value does not destroy the existing version, but rather creates a new version that exists alongside the previous version.

💡
Functional languages generally have persistent values and imperative languages ephemeral values.

Imperative / Ephemeral

To illustrate the difference, let's look at what happens when two Linked Lists are merged in an imperative setting:

After merging these two lists, the values x and y are destroyed:

For programs that require concurrent access to values, destructive updates (ie. assignments) can introduce complexity and difficulty reasoning about which values are assigned at any given time.

Functional / Persistent

Given the same values or arguments:

Merging the two lists makes copies of some data and the previous versions remain unchanged:

And in this case, x is still available to some concurrent operation or process.

Immutability or persistence offers an advantage in the design of large-scale concurrent or parallel applications such as those designed with Erlang/Elixir.

On the other hand, allowing mutation often means temporarily violating a property of the system and then restoring it.  Most of the time these operations cannot be interrupted and thus require thread locking machinery which can impact performance and be difficult to implement and debug.

💡
Erlang is also garbage collected and you can read more about how this works in the Erlang docs: https://www.erlang.org/doc/apps/erts/garbagecollection

Strict vs Lazy Evaluation

Lastly, I wanted to mention that Erlang and Elixir use strict evaluation order which means that function arguments are evaluated in order before the expressions in the function body.

In lazy languages, arguments are evaluated on-demand only when and if an operation needs the results to continue.  Haskell for example, is a lazily evaluated functional language. While lazy evaluation has many advantages, its main drawback is that memory usage becomes hard to predict.

In strictly evaluated languages like Elixir, it's more clear which expressions will be evaluated and when by reading the code which makes reasoning about worse-case running time or time complexity straightforward.

Wrapping up

Again, this was not intended to be a comprehensive dive into all the different aspects of functional vs imperative programming, but rather to briefly describe functional properties of Erlang/Elixir and provide an overview of the functional programming landscape including some strengths and weaknesses.  

To recap: Erlang/OTP is a compiled, impure, dynamically typed, strongly typed, declarative, expressive, garbage collected, strictly evaluated, functional programming language that runs on the BEAM and has immutable/persistent values, higher order functions, employs the Actor Model via processes and message passing with optional support for static analysis and a "Let it Crash" philosophy to handling exceptional cases.

By gaining perspective about the differences between functional and imperative languages we can better understand how and why data structures are implemented a certain way, which can in turn, inform design decisions when approaching problems in a functional setting.

Thanks for reading!

-- Troy

Twitter: @tmartin8080  | ElixirForum

Resources

Subscribe to ReadReplica

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe