0.6 C
New York
Saturday, February 22, 2025

Intro to Elixir: A fresh take on functional programming



To print this out we can use the IO.inspect function for pretty printing:


IO.inspect(book_lengths)
[17, 12, 9, 9, 21]

Elixir’s collection types

We’ve already seen the List type in action. Elixir includes these main collection types:

  • List: An immutable, but designed for modification-by-duplication, homogenous collection of arbitrary types.
    • Syntax: Square brackets with items:  [x,y,z]
  • Tuple: Designed for holding values primarily, not manipulation, tuples are like lists but geared towards read performance.  Think of them a data access kind of collection.
    • Syntax: Curly braces with items: {x,y,z}
  • Keywords List: Ordered key-value pairs, string-only keys, mainly used for named arguments for functions
    • Syntax: Square brackets with pairs: {x: x1,y: y1,z: z1}
  • Maps: The familiar key-value pairs, where keys can be anything and the collection is unordered.
    • Syntax: Percent curly braces with pairs:
      • %{x => x1, y => y1, z => z1}
      • %{x: x1, y: y1, z: z1}

Maps and atoms

Maps have two styles of declaration, and the one to use depends on whether the keys are atoms. An atom is a variable whose value is the same as its name, a kind of super-constant. An atom is declared with a colon followed by a literal.

We could create a map of string keys to integer values like so:


books_and_lengths = %{ "The Bhagavad Gita" => 17, "Tao Te Ching" => 12 }

The following is different, and creates a map of atoms to integers, probably not what we want in this case:


books_and_lengths = %{ "The Bhagavad Gita": 17, "Tao Te Ching": 12 }

Note the placement of the colon. In a Map, the colon being directly next to the kay indicates it’s an atom, and atoms can be quote-enclosed (to support otherwise illegal characters).

The bottom line is to use the arrow syntax (=>) when you want a normal variable and the key and the colon (:) when you want atoms.

Normally, atoms are declared like this:


:my_atom

Here’s another way to declare a map with atom keys:


my_map = %{:atom1 => “foo”, :atom2 => “bar”}

Modules

Elixir supports modules, which are namespaces that gather together related functions. It does not hold state or variables like a class or code block. As you’d expect, you can call other functions from within the same module, but those calling in from outside need to preface the calls or import the module.

Here’s a simple module:


defmodule BookFunctions do
  def myFunc
  end
end

BookFunctions.myFunc()

Pattern matching

Syntactic flavors and standard library characteristics go a long way to making up the overall feeling of using a language. They are the commonplace features you interact with all the time. But every language has some features that stand out.

Functional pattern matching is a sophisticated and lovely feature that Elixir brings to the table, allowing you to perform conditional function execution in a switch-like syntax. Let’s say we want to output small, medium, or long based on the book title lengths:


defmodule BookFunctions do
  def categorize_length(length) do
    case length do
      length when length <= 10 -> "Short"
      length when length &lt= 20 -> "Medium"
      _ -> "Long"
    end
  end

  def print_categories(lengths) do
    Enum.each(lengths, fn length ->
      category = categorize_length(length)
      IO.puts("#{length} characters: #{category}")
    end)
  end
end

A couple of notes:

  • BookFunctions is a module, which you’ve seen.
  • In Elixir, return statements are implied, so the categorize_length() function automatically returns whatever is the result of the last expression.

The case keyword is what creates the pattern-matching block, in the categorize_length function. The length when length syntax (technically, a guard clause) lets us do range checking on the length variable, and if it meets the criteria, the -> operator tells us what to return from the case. (Since this is the final statement of the function, it will also be the functional return value.)

We could use this new function on our book_lengths like so:


BookBookFunctions.print_categories(book_lengths)
17 characters: Medium
12 characters: Medium
9 characters: Short
9 characters: Short
21 characters: Long

Enum.each is analogous to forEach in other languages like JavaScript, letting us perform an operation on each element of a collection.

Looping

Elixir does not have for and while loops. This can be a bit shocking at first, but it is in line with the immutability favored by functional philosophy. In essence, Elixir doesn’t want you doing mutation during loops, and instead wants you to use recursion. Recursion keeps you in the realm of functions, and ideally, you want to use pure functions (meaning, functions without side-effects).

Much of the looping you need to do can be handled with functional operations like Enum.each and Enum.map. The Elixir forum has a good, extensive discussion of looping and alternatives.

Comprehensions

One of the most direct ways to simulate a for loop is with comprehensions, which you would be forgiven for mistaking for an actual for loop:


for x <- 0..10, do: IO.puts x

See the Elixir docs for more on comprehensions and how they simplify collection operations.

Pipe operator

The pipe operator gives you a clean syntax for chaining function results together. Think of it as a more elegant form of nesting functions. Here’s a simple example of the pipe operator on our books_and_lengths collection:


books_and_lengths
  |> Map.keys() 
  |> Enum.map(&String.upcase/1) 
  |> Enum.join(", ") 
  |> IO.puts() 

The output is:


The Bhagavad Gita, Tao Te Ching

Concurrency

Although concurrency is a complex topic, it’s one of Elixir’s areas of strength, so let’s take a quick look. Elixir uses Actors, something like virtual threads in that they are not full operating-system processes. Actors support message-passing for simplified concurrent communication.

The following example, demonstrating message handling, is from the Elixir docs:


defmodule Example do
  def listen do
    receive do
      {:ok, "hello"} -> IO.puts("World")
    end

    listen()
  end
end

Notice that the listen function is recursive (it calls itself at the end), which lets it handle multiple messages. Without the recursion the process would exit.

To launch this Actor, we use spawn:


pid = spawn(Example, :listen, [])

Then we can send a message from the main process, using the pid we saved:


send pid, {:ok, "hello"}

This outputs “World” to the console.

Conclusion

Languages are defined largely by what they make easy and what they make hard. Elixir is clearly devoted to making it easy for a programmer to stay in the functional programming mentality, and harder to stray into mutations and side-effects. 

The overall effect is that you tend to write good functional programming code, as long as you work with the language and don’t fight it. It’s not hard to see why Elixir has captured so much interest, bringing Erlang’s legacy into the modern world. It’s a programming language with strong ideas about how to do things and an active, enthusiastic community.


Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles