DEV Community

Cover image for Functional Programming and Erlang Platform
Muzhawir Amri
Muzhawir Amri

Posted on • Edited on • Originally published at muzhawir.com

Functional Programming and Erlang Platform

📜 You can read the Indonesian version here!

Table of Contents

Functional Programming Paradigm

Every programming language is built on top of one or more programming paradigms, which are ways of thinking that influence how we write code, manage data, and design solutions. Understanding these paradigms helps us choose the most effective approach to solving a problem.

Let’s see a few commonly used paradigms.

Imperative Programming

The imperative paradigm takes the approach of giving the computer step-by-step instructions on how to accomplish a task. We explicitly and thoroughly control the sequence of actions, from initialization to final execution.

Example in Python:

total = 0

for i in range(1, 6):
    total += i

print("Total:", total)
Enter fullscreen mode Exit fullscreen mode

This code adds numbers from 1 to 5. Every process is explicit: we initialize a variable, run a loop, sum the values, and print the result. Languages like Python, C, and Java are strongly supportive of this paradigm.

Declarative Programming

The declarative paradigm focuses on what should be achieved, not how to achieve it technically. We declare the desired outcome, and the compiler or runtime takes care of the execution details.

Example in SQL:

SELECT * FROM users WHERE is_active = 1;
Enter fullscreen mode Exit fullscreen mode

Here we don't write step-by-step data fetching logic. We simply state the goal: retrieving active users, and SQL handles the implementation details. This paradigm is also seen in HTML and regular expressions, where we describe the end result without dictating the process flow explicitly.

Object-Oriented Programming (OOP)

The object-oriented paradigm structures applications as collections of objects. Each object stores data (state) and behavior (methods) to process that data. These objects interact with each other to form the application logic.

Example in Java:

class Car {
    String brand = "Toyota";

    void moveForward() {
        System.out.println(brand + " is moving forward -->");
    }

    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.moveForward();
    }
}
Enter fullscreen mode Exit fullscreen mode

We define a Car class with a brand attribute and a moveForward() method. Then we create a myCar object and call its method. Languages like Java, Python, and JavaScript support this paradigm and encourage modularity and code reuse.

Functional Programming (FP)

The functional paradigm builds programs from many pure functions, which accept input, produce output, and do not cause side effects. This means a function’s result only depends on the input provided, without relying on or modifying external state. Data is processed through a series of functions that can be composed to form more complex logic.

Example in Elixir:

numbers = [1, 2, 3, 4, 5]

numbers
|> Enum.filter(fn x -> rem(x, 2) == 0 end)
|> Enum.map(fn x -> x * 2 end)
Enter fullscreen mode Exit fullscreen mode

We start with a list of numbers, then use the filter function to select even numbers, and pass the result to the map function to double them. No variables are modified, and all functions can be tested independently. Languages like Erlang and Elixir are designed to support this style.

Paradigms Used by Erlang and Elixir

Erlang and Elixir combine two main paradigms:

  • Functional - Programs are built from many pure functions that can be composed to create complex logic.
  • Declarative - We focus on the desired result, not the sequence of instructions. Execution details are handled by the runtime.

This combination leads to code that is concise, modular, and easy to test. It is ideal for building large-scale systems that are fault-tolerant, can run in a distributed manner across multiple machines, and remain continuously available. For these reasons, the paradigm is widely used in telecommunications systems, real-time applications, and backend services that demand high reliability.

Basic Principles of Functional Programming

Functional programming is an approach to writing code that differs from the imperative style. In the imperative approach, we write instructions step by step: do this, then that, change this value, and so on. In functional programming, we focus on how data flows and is transformed through a series of functions.

We build programs by composing small functions. Data is passed into a function, which processes it and produces a new value, which can then be passed to the next function. This approach makes the program’s flow clearer and less surprising.

To make this model work well, functional programming uses several key principles:

Immutability

In functional programming, data is immutable. This means once a value is created, it cannot be changed. If we want to “change” a value, the system actually creates a new modified copy, while the original data remains untouched.

This approach is called data transformation, not modification, because we do not alter the old data but create a new version of it.

Even though we haven’t fully covered Elixir yet, let’s look at a quick example. Each example below includes a brief explanation of what the code does.

list = [1, 2, 3]
new_list = [0 | list]  # Result: [0, 1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

The list variable still holds [1, 2, 3] unchanged in memory. Meanwhile, new_list is a new list created by prepending 0 to it. This value is stored in a different memory location.

Immutability offers several benefits:

  • Avoids bugs caused by data being changed elsewhere.
  • Makes data flow easier to track and understand.
  • Prevents hidden or unpredictable state

Pure Function

A pure function has two main characteristics:

  1. Its output is always the same for the same input. Whenever and wherever it is called, the result is consistent.
  2. It does not cause side effects. It does not print to the screen unexpectedly, write to files, modify external data, or secretly read values from outside.

Example in Elixir:

square = fn x -> x * x end
Enter fullscreen mode Exit fullscreen mode

This is an anonymous function stored in the square variable. If we call square.(4) multiple times, the result will always be 16. This function does not depend on any global state and does nothing beyond the x * x calculation.

Why are pure functions important?

  • Easy to test: just provide input and check the result. There is no external influence, so results are consistent.
  • Easy to compose: since they don’t have side effects, we can combine them without worrying about interference between functions.
  • Easy to understand: we can read the function’s contents without needing to know the global application context.

Of course, not all functions can be pure. Writing to files, accessing databases, or sending HTTP requests all require side effects.

In Elixir and many other modern functional languages, we can still write functions that perform side effects. However, the recommended approach is to separate side effects from the program’s core logic.

This means we try to keep most functions pure, focusing solely on transforming data without relying on or changing the outside world. Meanwhile, functions that deal with side effects are placed in specific parts of the code that are responsible for external interactions, such as controllers in web applications that handle HTTP requests or perform file/database access.

The goal is to keep most of the code clean, testable, and free of unexpected effects. We know exactly which parts of the code can be tested purely and which must be run in a real-world context like internet or file system interactions.

First-Class Function

In the functional paradigm, functions are treated like regular values. This means functions can be stored in variables, passed as arguments to other functions, and returned as results from other functions.

Quick example in Elixir:

double = fn x -> x * 2 end

Enum.map([1, 2, 3], double)
Enter fullscreen mode Exit fullscreen mode

Here, double is an anonymous function stored in a variable. Then, in the Enum.map function, we pass double as an argument that will be applied to each element of the list [1, 2, 3] when Enum.map is called.

Erlang Development Platform

Before we learn Elixir, it's important to understand that it runs on top of Erlang. Without Erlang, Elixir wouldn’t run.

Erlang is a development platform created by the Swedish telecommunications company Ericsson in the mid-1980s. Its original goal was to build telephone network systems that could run continuously, even under extreme conditions such as high call volume, software bugs, or hardware/software upgrades. In short, the system had to remain operational and available despite facing various disruptions.

The name "Erlang" is short for Ericsson Language and was developed by three main figures: Joe Armstrong, Robert Virding, and Mike Williams. Initially, it was used internally at Ericsson. But since its release as open-source software in 1998, Erlang has been widely adopted in industries such as telecommunications, banking, multiplayer gaming, and real-time systems.

Over time, Erlang has proven effective in handling large-scale systems that require high availability and fault tolerance. This is why Elixir chose Erlang as its foundation.

Systems That Stay Available

Joe Armstrong once said in an interview with Rackspace in 2013:

“If Java is write once, run anywhere. Erlang is write once, run forever.”

This statement captures Erlang’s core philosophy: to create systems that keep running even when some components fail.

To support this, Erlang was designed with the following principles:

  • Fault Tolerance - The system keeps running even if some processes crash. These processes can be replaced or recovered automatically.
  • Scalability - The system can handle more load without being shut down, whether by adding CPU, RAM, or running additional nodes on other machines.
  • Distributed System - Erlang allows multiple nodes to connect and work as a single unit. This facilitates data replication, load distribution, and protects against single points of failure that could take down the whole system.
  • Responsiveness - Erlang systems are designed to handle heavy loads and high demand quickly, even under sudden spikes like traffic surges or mass call events.
  • Hot Code Swapping - Erlang allows live code updates without stopping the service. This is critical for systems that must remain continuously online.

Concurrency in Erlang

From the beginning, Erlang was designed to handle large-scale concurrency. Systems like phone networks, banking services, and real-time communication platforms must be able to run many units of work simultaneously without interference.

Unlike complex and heavyweight OS threads or processes, Erlang uses lightweight entities called Erlang processes. These are not OS threads or processes, but lightweight units of work scheduled by the Erlang virtual machine called BEAM.

They are extremely small and can be created in huge numbers, even millions, which makes them ideal for massive concurrency. We can run many processes efficiently, with low latency and strong isolation.

Key advantages of Erlang processes:

  • Full process isolation - Each process has its own memory and execution flow. If one process crashes, others continue unaffected. Because no memory is shared, there are no side effects or interference between processes.
  • Asynchronous message passing - All processes communicate by sending messages to one another. Since messages are passed by value (not by reference), each process works with its own copy of the data. This removes the need for locks or synchronization like in thread-based systems. There is no contention or waiting, so race conditions are avoided.
  • Per-process garbage collection - Each process manages its own memory, including garbage collection. This means when one process is freeing unused memory, others continue running uninterrupted. There is no global pause.
  • Lightweight and efficient - BEAM can run millions of processes efficiently. Erlang processes consume minimal memory and CPU. The BEAM scheduler fairly distributes execution time among all processes.

A Complete Development Platform

Erlang is not just a programming language. It is a full development platform that includes four key components:

  1. Erlang Language - The functional programming language on this platform. Erlang code is compiled into bytecode that runs on the BEAM virtual machine.
  2. BEAM Virtual Machine - The virtual machine that runs all Erlang (and Elixir) processes. BEAM handles process scheduling, memory management, message passing, and process isolation.
  3. OTP (Open Telecom Platform) - A set of libraries and abstractions ready to use for building robust and structured systems. OTP is an inseparable part of Erlang, and together they are often called Erlang/OTP.
  4. Development tools - Erlang provides tools like the interactive shell (erl), debugger, profiler, and more. These tools help us develop, test, and deploy applications on the platform.

Erlang is an actively developed open-source project with regular releases. Its source code is available on GitHub Erlang/OTP, and Ericsson remains directly involved in its development.

References

Top comments (1)

Collapse
 
hammglad profile image
Hamm Gladius

I’m excited to start applying the functional programming principles from this post, especially focusing on writing pure functions and keeping data immutable. I’ll try refactoring some of my existing code in Elixir to be more modular and testable, and I want to explore Erlang’s concurrency features for building more reliable systems.