Skip to content

1. Elixir Introduction

1. Introduction

Elixir is a dynamic, functional programming language designed for building scalable and maintainable applications. It runs on the Erlang Virtual Machine (BEAM), which is known for its low-latency, distributed, and fault-tolerant systems. Elixir is widely used for web development, real-time applications, and distributed systems due to its concurrency capabilities and fault-tolerance features.

1.1 Key Features of Elixir:

  • Functional Programming: Focuses on immutability and first-class functions.
  • Concurrency: Uses lightweight processes to handle concurrent tasks efficiently.
  • Fault Tolerance: Built on Erlang’s “let it crash” philosophy, allowing systems to recover from failures gracefully.
  • Metaprogramming: Supports macros, allowing code to generate code.
  • Scalability: Designed to handle large-scale distributed applications.

1.2 Pros of Elixir

1. Concurrency and Scalability:

  • Elixir excels in handling many concurrent processes due to its lightweight process model (inherited from Erlang). This makes it ideal for real-time applications, chat systems, IoT, and any situation requiring high concurrency.

2. Fault Tolerance:

  • Built on the Erlang VM (BEAM), Elixir inherits a “let it crash” philosophy. The language is designed for building fault-tolerant systems that can recover automatically from failures, making it suitable for highly reliable, distributed systems.

3. Functional Programming:

  • Elixir’s functional nature encourages immutability and pure functions, leading to more predictable and maintainable code. This paradigm reduces side effects, making code easier to reason about, test, and debug.

4. Metaprogramming:

  • Elixir supports powerful metaprogramming through macros, allowing developers to write code that generates code. This can lead to more flexible and reusable codebases.

5. Active Community and Ecosystem:

  • Elixir has a growing community and a rich ecosystem of libraries and tools, including Phoenix (a popular web framework). The community is known for its friendliness and support.

6. Ease of Deployment:

  • Elixir’s deployment story is strong, especially with tools like Distillery and Mix, which make it easy to build and deploy releases across distributed systems.

7. Real-time Capabilities:

  • Elixir, particularly with the Phoenix framework, offers built-in support for real-time features like websockets and channels, making it a top choice for real-time applications.

1.3 Cons of Elixir

1. Learning Curve:

  • For developers coming from imperative or object-oriented backgrounds, the functional programming paradigm, as well as concepts like immutability and recursion, can take some time to master.

2. Performance:

  • While Elixir performs well in I/O-bound and concurrent tasks, it may not match the raw computational performance of languages like C, Rust, or even Java for CPU-bound tasks. This is due to its dynamic nature and the overhead of running on the BEAM.

3. Smaller Talent Pool:

  • Compared to languages like JavaScript, Python, or Java, the pool of Elixir developers is smaller. This can make it more difficult to hire Elixir developers, especially those with experience in large-scale production environments.

4. Tooling and Libraries:

  • Although Elixir’s ecosystem is robust and growing, it still lags behind older, more established languages in terms of the breadth and depth of available libraries and tools.

5. Maturity:

  • Elixir, while stable and mature in many ways, is still relatively young compared to languages like Java or Python. This can sometimes result in less mature tooling, fewer enterprise-level case studies, and a more experimental feel in certain areas.

6. Niche Use Cases:

  • Elixir is fantastic for concurrent, distributed, and fault-tolerant systems but may be overkill for simpler projects where such features are not needed. For basic CRUD apps or simple scripts, more straightforward languages might be a better fit.

7. Deployment Complexity in Certain Environments:

  • While Elixir’s deployment story is strong, deploying distributed Elixir systems can be complex, especially for teams unfamiliar with Erlang/BEAM architecture. Handling node clustering and network partitions requires a deep understanding of the underlying system.

1.4 Comparison Between Elixir and C:

FeatureElixirC
ParadigmFunctional, ConcurrentProcedural, Low-level
Memory ManagementAutomatic (Garbage Collection)Manual (malloc/free)
Concurrency ModelActor model with lightweight processesThreads, but manual handling required
CompilationRuns on BEAM (Erlang VM), compiled to bytecodeCompiled to machine code, highly optimized
PerformanceHigh for I/O-bound tasks, not CPU-boundHigh performance, especially for CPU-bound tasks
Fault ToleranceHigh (let it crash philosophy)Limited, must be managed manually
Use CasesWeb apps, real-time systems, distributed appsSystem programming, embedded systems, performance-critical applications
SyntaxDynamic and expressiveLow-level, closer to hardware
Type SystemDynamic typingStatic typing
Standard LibraryExtensive, high-level functionsMinimal, relies heavily on external libraries

1.5 Key Differences:

1. Programming Paradigm:

  • Elixir: Emphasizes functional programming, where functions are first-class citizens and data is immutable.
  • C: Procedural and imperative, focusing on explicit control flow and mutable state.

2. Concurrency:

  • Elixir: Built-in concurrency support using the actor model, allowing easy management of millions of lightweight processes.
  • C: Requires manual management of threads, synchronization, and shared memory, which is complex and error-prone.

3. Memory Management:

  • Elixir: Automatic memory management with garbage collection.
  • C: Manual memory management, giving more control but also requiring careful handling to avoid memory leaks or corruption.

4. Performance:

  • C: Typically faster and more efficient for CPU-bound tasks due to direct compilation to machine code.
  • Elixir: Optimized for I/O-bound and concurrent tasks, but not as fast for raw computation.

5. Fault Tolerance:

  • Elixir: Highly resilient, designed for long-running, distributed systems that need to recover gracefully from failures.
  • C: Fault tolerance needs to be manually implemented, and errors can lead to undefined behavior or crashes.

6. Development Speed:

  • Elixir: Higher-level abstractions and built-in libraries result in faster development cycles for complex applications.
  • C: Lower-level control requires more code and a deeper understanding of the system, leading to longer development times for complex tasks.

2. Getting Started

To run Elixir on Ubuntu, follow these steps to install and get started:

2.1 Install Elixir

First, ensure your system is up-to-date:

Terminal window
sudo apt-get update

install Elixir:

Terminal window
sudo apt-get install -y elixir

2.2 Verify Installation

After installation, check if Elixir was installed correctly:

Terminal window
elixir -v

This command should display the installed version of Elixir.

2.3 Running Elixir Code

You can run Elixir in several ways:

2.3.1 Interactive Elixir (IEx)

To start an interactive Elixir shell, run:

Terminal window
iex

In the iex shell, you can enter Elixir code and get immediate feedback:

iex> IO.puts("Hello, World!")
Hello, World!
:ok

2.3.2 Running a Script

Create a file named example.exs:

Terminal window
nano example.exs

Add the following Elixir code:

IO.puts("Hello, Elixir!")

Save and exit. Then run the script with:

Terminal window
elixir example.exs

You should see:

Terminal window
Hello, Elixir!

3. Data Structures in Elixir

In Elixir, data structures and control structures are designed to work harmoniously with its functional programming paradigm. Here’s an overview along with examples:

1. Tuples:

  • Tuples are fixed-size collections of values. They are often used to group related values.
  • Example:
    person = {"John", 30, :male}
    IO.puts elem(person, 1) # Outputs: 30

2. Lists:

  • Lists are linked lists, which means they are efficient for prepending elements but can be slow for indexing.
  • Example:
    fruits = ["apple", "banana", "cherry"]
    IO.puts hd(fruits) # Outputs: apple

3. Maps:

  • Maps are key-value stores, useful when you need to associate keys with values.
  • Example:
    person = %{"name" => "John", "age" => 30}
    IO.puts person["name"] # Outputs: John

4. Keyword Lists:

  • A special list of tuples where the first element is an atom. Often used for options.
  • Example:
    options = [width: 200, height: 100]
    IO.puts options[:width] # Outputs: 200

5. Structs:

  • Structs are special maps with a fixed set of keys and default values, used to create custom data types.
  • Example:
    defmodule Person do
    defstruct name: "John", age: 30
    end
    person = %Person{name: "Alice"}
    IO.puts person.name # Outputs: Alice

6. Binaries and Strings:

  • Binaries are sequences of bytes, and strings are UTF-8 encoded binaries.
  • Example:
    binary = <<104, 101, 108, 108, 111>> # "hello" in binary
    IO.puts binary # Outputs: hello

4. Control Structures in Elixir

1. if/else:

  • Conditional execution based on a boolean value.
  • Example:
    age = 18
    if age >= 18 do
    IO.puts "Adult"
    else
    IO.puts "Minor"
    end

2. unless/else:

  • The opposite of if, it executes the block if the condition is false.
  • Example:
    unless is_nil(nil) do
    IO.puts "Not nil"
    else
    IO.puts "Is nil"
    end

3. case:

  • Pattern matching against multiple possible values.
  • Example:
    case {1, 2, 3} do
    {1, x, 3} -> IO.puts "Matched with x = #{x}"
    _ -> IO.puts "No match"
    end

4. cond:

  • Similar to if/else, but evaluates multiple conditions.
  • Example:
    cond do
    2 + 2 == 5 -> "Math is wrong"
    2 + 2 == 4 -> IO.puts "Math is correct"
    true -> "Default"
    end

5. with:

  • Used to chain pattern matches that depend on each other, often for more readable code.
  • Example:
    with {:ok, data} <- fetch_data(),
    {:ok, processed} <- process_data(data) do
    IO.puts "Success"
    else
    _ -> IO.puts "Failure"
    end

5. Data Structure - Tuple

Tuples in Elixir are versatile data structures that can store a fixed number of elements. Here are some additional examples to illustrate their usage:

5.1 Basic Tuple

  • A tuple can hold different data types, such as integers, strings, atoms, or other tuples.
  • Example:
    coordinates = {40.7128, -74.0060} # Tuple representing latitude and longitude
    IO.puts elem(coordinates, 0) # Outputs: 40.7128
    IO.puts elem(coordinates, 1) # Outputs: -74.0060

5.2 Pattern Matching with Tuples

  • You can use pattern matching to destructure tuples and extract their values.
  • Example:
    {x, y} = {10, 20}
    IO.puts x # Outputs: 10
    IO.puts y # Outputs: 20

5.3 Returning Multiple Values from a Function

  • Tuples are often used to return multiple values from a function.
  • Example:
    defmodule Math do
    def add_and_subtract(a, b) do
    {a + b, a - b}
    end
    end
    {sum, difference} = Math.add_and_subtract(10, 4)
    IO.puts sum # Outputs: 14
    IO.puts difference # Outputs: 6

5.4 Tagged Tuples

  • A common pattern in Elixir is using a tuple where the first element is an atom that “tags” the data (often used for success/error handling).
  • Example:
    {:ok, result} = {:ok, 42}
    IO.puts result # Outputs: 42
    {:error, message} = {:error, "Something went wrong"}
    IO.puts message # Outputs: Something went wrong
  • Tuples can store related data together, like a mini-record.
  • Example:
    person = {"John", 30, :developer}
    IO.puts "Name: #{elem(person, 0)}"
    IO.puts "Age: #{elem(person, 1)}"
    IO.puts "Occupation: #{elem(person, 2)}"

5.6 Nested Tuples

  • Tuples can contain other tuples, which is useful for organizing complex data.
  • Example:
    nested_tuple = { {1, 2}, {3, 4}, {5, 6} }
    IO.inspect elem(nested_tuple, 1) # Outputs: {3, 4}

5.7 Updating Tuples

  • Tuples are immutable, so to “update” a tuple, you create a new one with the desired changes.
  • Example:
    tuple = {1, 2, 3}
    new_tuple = put_elem(tuple, 1, 42)
    IO.inspect new_tuple # Outputs: {1, 42, 3}

5.8 Tuple Size

  • You can check the size of a tuple using tuple_size/1.
  • Example:
    tuple = {:apple, :banana, :cherry}
    IO.puts tuple_size(tuple) # Outputs: 3

6. Data Structure - List

Lists in Elixir are fundamental data structures that are commonly used for storing collections of elements. Here are some examples to showcase how you can work with lists:

6.1 Creating and Accessing Lists

  • Lists are defined using square brackets, and elements can be accessed using the hd/1 (head) and tl/1 (tail) functions.
  • Example:
    fruits = ["apple", "banana", "cherry"]
    IO.puts hd(fruits) # Outputs: apple (head of the list)
    IO.inspect tl(fruits) # Outputs: ["banana", "cherry"] (tail of the list)

6.2 Concatenating Lists

  • You can concatenate lists using the ++/2 operator.
  • Example:
    list1 = [1, 2, 3]
    list2 = [4, 5, 6]
    combined = list1 ++ list2
    IO.inspect combined # Outputs: [1, 2, 3, 4, 5, 6]

6.3 Subtracting Lists

  • Use the --/2 operator to remove elements from a list.
  • Example:
    numbers = [1, 2, 3, 4, 5]
    filtered = numbers -- [2, 4]
    IO.inspect filtered # Outputs: [1, 3, 5]

6.4 Pattern Matching with Lists

  • You can destructure lists using pattern matching.
  • Example:
    [head | tail] = [10, 20, 30, 40]
    IO.puts head # Outputs: 10
    IO.inspect tail # Outputs: [20, 30, 40]

6.5 Recursively Processing Lists

  • Lists are commonly processed recursively in Elixir.
  • Example: Summing all elements in a list.
    defmodule Math do
    def sum([]), do: 0
    def sum([head | tail]), do: head + sum(tail)
    end
    IO.puts Math.sum([1, 2, 3, 4]) # Outputs: 10

6.6 List Comprehensions

  • List comprehensions provide a concise way to generate lists or perform operations on lists.
  • Example: Doubling each element in a list.
    numbers = [1, 2, 3, 4]
    doubled = for n <- numbers, do: n * 2
    IO.inspect doubled # Outputs: [2, 4, 6, 8]

6.7 Filtering Lists

  • You can filter lists based on a condition using list comprehensions.
  • Example: Filtering even numbers.
    numbers = [1, 2, 3, 4, 5, 6]
    evens = for n <- numbers, rem(n, 2) == 0, do: n
    IO.inspect evens # Outputs: [2, 4, 6]

6.8 Appending Elements to a List

  • Since lists are linked lists, appending elements to the end requires creating a new list.
  • Example:
    list = [1, 2, 3]
    new_list = list ++ [4]
    IO.inspect new_list # Outputs: [1, 2, 3, 4]

6.9 Reversing a List

  • You can reverse a list using the Enum.reverse/1 function.
  • Example:
    list = [1, 2, 3]
    reversed = Enum.reverse(list)
    IO.inspect reversed # Outputs: [3, 2, 1]

6.10 Finding the Length of a List

  • Use the length/1 function to determine the number of elements in a list.
  • Example:
    list = ["apple", "banana", "cherry"]
    IO.puts length(list) # Outputs: 3

6.11 Inserting an Element at the Beginning of a List

  • Prepending an element to a list is efficient and straightforward using the | operator.
  • Example:
    list = [2, 3, 4]
    new_list = [1 | list]
    IO.inspect new_list # Outputs: [1, 2, 3, 4]

6.12 Checking if a List is Empty

  • You can check if a list is empty by pattern matching or using the == operator.
  • Example:
    empty_list = []
    IO.puts length(empty_list) == 0 # Outputs: true
    IO.puts empty_list == [] # Outputs: true

7. Control Structure - Loops

Elixir uses recursion or higher-order functions like Enum and Stream instead of traditional loops. Here’s an example using Enum.each to loop through a list:

Enum.each([1, 2, 3, 4, 5], fn x ->
IO.puts("Number: #{x}")
end)

This code loops through the list [1, 2, 3, 4, 5] and prints each number.

You can also use recursion:

defmodule LoopExample do
def print_numbers([]), do: :ok
def print_numbers([head | tail]) do
IO.puts("Number: #{head}")
print_numbers(tail)
end
end
LoopExample.print_numbers([1, 2, 3, 4, 5])

This recursive function achieves the same result.

8. Functions

8.1 Writing a simple Function

In Elixir, functions can be defined inside a module. Here’s a basic example of how to write a function:

defmodule MyModule do
def greet(name) do
"Hello, #{name}!"
end
end
  • defmodule MyModule do: Defines a module named MyModule.
  • def greet(name) do: Defines a function greet that takes one argument, name.
  • "Hello, #{name}!": Returns a greeting string.
  • end: Closes the function and module.

You can call the function like this:

MyModule.greet("Alice") # Returns "Hello, Alice!"

Elixir functions can also be written in a one-liner if they are simple:

defmodule MyModule do
def greet(name), do: "Hello, #{name}!"
end

##8.Fnction that returns no value

In Elixir, a function that returns no meaningful value usually returns the atom :ok or :nil. Here’s an example:

defmodule MyModule do
def log_message(message) do
IO.puts("Logging: #{message}")
:ok
end
end
  • log_message/1 prints a message to the console using IO.puts/1 and then returns :ok.
  • The function doesn’t return a value that’s meant to be used elsewhere, so :ok is used to signify successful completion.

You can call this function like so:

MyModule.log_message("Engine started") # Outputs "Logging: Engine started" and returns :ok

8.2 Function name and Arity

In Elixir (and other functional programming languages), the terms arity and function name are essential concepts for understanding how functions work.

8.2.1 Function Name

  • The function name is simply the identifier you give to a function. It is what you use to call the function in your code.
  • Example:
    defmodule Math do
    def add(a, b) do
    a + b
    end
    end
    • In this example, add is the function name.

8.2.2 Arity

  • Arity refers to the number of arguments a function takes. It is an integral part of how functions are identified in Elixir.
  • Functions with the same name but different arities are treated as different functions.
  • In Elixir, the arity is often explicitly mentioned alongside the function name, such as add/2, where 2 indicates that the function takes two arguments.

Examples:

1. Single Arity Function:

defmodule Greeter do
def hello(name) do
"Hello, " <> name
end
end
  • The function hello/1 has an arity of 1 because it takes one argument.

2. Multiple Functions with Different Arities:

defmodule Math do
def add(a, b) do
a + b
end
def add(a, b, c) do
a + b + c
end
end
  • Here, add/2 and add/3 are two different functions with different arities. The first takes two arguments, and the second takes three.

Why is Arity Important?

  • Function Identification: In Elixir, functions are uniquely identified by both their name and their arity. This allows you to define multiple functions with the same name but different numbers of arguments.

  • Pattern Matching: Arity is essential when using pattern matching. Functions with different arities can have different behavior based on the number of arguments provided.

8.3 Help on Functions

In Elixir, you can get help or documentation for a function using the h helper in an interactive Elixir shell (IEx). The h command provides detailed information about the function, including its purpose, arguments, and examples.

8.3.1 Steps to Get Help for a Function in Elixir:

1. Start IEx:

  • Open your terminal and start the interactive Elixir shell by typing:
    Terminal window
    iex

2. Use the h Helper:

  • To get help for a specific function, use the h helper followed by the module name and the function name, along with its arity. For example:
    h Enum.map/2
  • This command will display the documentation for the Enum.map/2 function.

8.3.2 Example in IEx:

iex> h Enum.map/2
def map(enumerable, fun)
Returns a new enumerable where each item is the result of invoking `fun` on each
corresponding item of `enumerable`.
## Examples
iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]

8.3.3 Additional Tips:

  • If you need help with all the functions in a module, you can type h ModuleName:
    h Enum
  • To get help on Elixir’s operators or other special constructs, you can use h with the operator:
    h Kernel.+/2

9. Pipe Operator

The |> operator in Elixir is known as the pipe operator. It is used to pass the result of one function as the first argument to the next function in a chain, creating a more readable and concise flow of data through multiple functions.

9.1 How the Pipe Operator Works

  • The expression on the left side of the |> operator is passed as the first argument to the function on the right side.
  • This allows for a clean and natural flow, especially when chaining multiple function calls.

9.2 Simple Example

Here’s a basic example using the pipe operator to manipulate a list:

# Without the pipe operator
list = [1, 2, 3, 4]
result = Enum.map(list, fn x -> x * 2 end)
final_result = Enum.filter(result, fn x -> x > 4 end)
IO.inspect(final_result) # Outputs: [6, 8]
# Using the pipe operator
final_result =
[1, 2, 3, 4]
|> Enum.map(fn x -> x * 2 end)
|> Enum.filter(fn x -> x > 4 end)
IO.inspect(final_result) # Outputs: [6, 8]

9.3 Explanation

  • Without the pipe operator: The result of each function must be stored in a variable before passing it to the next function, which can lead to nested or repetitive code.
  • With the pipe operator: The flow is more readable and intuitive. The list [1, 2, 3, 4] is passed through the Enum.map/2 function, and its result is automatically passed to Enum.filter/2.

9.4 Additional Example

Using the pipe operator to transform a string:

result =
"elixir is awesome"
|> String.upcase()
|> String.split(" ")
|> Enum.join("-")
IO.puts(result) # Outputs: ELIXIR-IS-AWESOME