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.
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.
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:
- Rust Example:
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
- the value must be a positive integer (unsigned)
- the value must be 32-bits or
0 to 232 - 1
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:
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.
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.
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.
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"]
ias 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.
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
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.
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.
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!