DEV Community

Cover image for Learning Elixir: Understanding Atoms, Booleans and nil
João Paulo Abreu
João Paulo Abreu

Posted on • Edited on

Learning Elixir: Understanding Atoms, Booleans and nil

Atoms are one of Elixir's fundamental data types, serving as constants whose value is their own name. Along with booleans and nil, they form the backbone of symbolic computation and control flow in Elixir. Understanding how to effectively use these types is crucial for writing idiomatic Elixir code.

Note: The examples in this article use Elixir 1.17.3. While most operations should work across different versions, some functionality might vary.

Table of Contents

Introduction

Atoms in Elixir are constants that represent their own name. Combined with booleans (true and false) and nil, they provide the foundation for control flow and state representation in Elixir applications.

Understanding Atoms

Basic Atom Concepts

# Creating atoms
iex> :hello
:hello
iex> :"hello world"
:"hello world"

# Atoms are equal to themselves
# == checks for value equality
iex> :hello == :hello
true

# Converting atoms to strings and back
iex> Atom.to_string(:hello)
"hello"
iex> String.to_atom("hello")
:hello

# Atoms represent constant values
# === checks for both value and type equality (more strict)
iex> :hello === :hello
true
Enter fullscreen mode Exit fullscreen mode

Atoms vs Strings

# String comparison
iex> string1 = "hello"
"hello"
iex> string2 = "hello"
"hello"
iex> string1 == string2  # Compares content
true

# Atom comparison
iex> atom1 = :hello
:hello
iex> atom2 = :hello
:hello
iex> atom1 === atom2    # Atoms with the same name are identical
true

# Conversion between strings and atoms
iex> Atom.to_string(:hello)
"hello"
iex> String.to_atom("world")
:world
Enter fullscreen mode Exit fullscreen mode

While strings can represent dynamic data, atoms are like labels that act as unique identifiers.

Analogy: If strings are like sticky notes you can write on, atoms are like permanent, engraved name tags—they are fixed and efficient.

Contexts for Atoms and Strings

Atoms and strings are often used in distinct scenarios in Elixir:

Context Atoms Strings
State Representation :ok, :error, :pending Not typically used
Configuration Keys :format, :option Rarely used
Dynamic User Input Not applicable Usernames, comments, etc.
Pattern Matching Frequently used Rarely used

Atom Creation and Naming

Naming Conventions

# Standard atoms
iex> :ok
:ok
iex> :error
:error
iex> :undefined
:undefined

# Atoms with special characters
iex> :"hello-world"
:"hello-world"
iex> :"hello_world"
:hello_world
iex> :"Hello World!"
:"Hello World!"

# Module atoms (automatically created)
iex> String
String
iex> is_atom(String)
true
Enter fullscreen mode Exit fullscreen mode

Dynamic Atom Creation

# Converting strings to atoms
iex> String.to_existing_atom("error")
:error
iex> String.to_existing_atom("unknown_atom")
** (ArgumentError) errors were found at the given arguments:
  * 1st argument: not an already existing atom
    :erlang.binary_to_existing_atom("unknown_atom", :utf8)

# Creating new atoms from strings
iex> String.to_atom("dynamic_status")
:dynamic_status
Enter fullscreen mode Exit fullscreen mode

Common Use Cases

Return Values and Status

# Common return tuples
iex> {:ok, "success"}
{:ok, "success"}
iex> {:error, "something went wrong"}
{:error, "something went wrong"}

# Pattern matching on return values
iex> result = {:ok, "success"}
case result do
  {:ok, message} -> "Success: #{message}"
  {:error, message} -> "Error: #{message}"
end
"Success: success"
Enter fullscreen mode Exit fullscreen mode

Configuration and Options

# Option handling
# Run this code in IEx to test the functionality
defmodule Printer do
  def print(text, format: format) do
    case format do
      :uppercase -> String.upcase(text)
      :lowercase -> String.downcase(text)
      _ -> text
    end
  end
end

iex> Printer.print("Hello", format: :uppercase)
"HELLO"
iex> Printer.print("Hello", format: :lowercase)
"hello"
Enter fullscreen mode Exit fullscreen mode

State Representation

# State machine example
# Run this code in IEx to test the functionality
defmodule TaskState do
  # Pattern matching on atoms to define state transitions
  # Each function handles a specific state
  def next_state(:pending), do: :in_progress
  def next_state(:in_progress), do: :completed
  def next_state(:completed), do: :archived
  # Catch-all function for invalid states
  def next_state(_), do: :error
end

iex> TaskState.next_state(:pending)
:in_progress
iex> TaskState.next_state(:in_progress)
:completed
Enter fullscreen mode Exit fullscreen mode

Pattern Matching with Atoms

Basic Pattern Matching

# Function clauses with atoms
# Example: Handling different response states in a web API
defmodule ResponseHandler do
  # inspect/1 converts any Elixir term into a readable string
  def handle({:ok, data}), do: "Got data: #{inspect(data)}"
  def handle({:error, reason}), do: "Error: #{reason}"
  def handle(:skip), do: "Skipping"
end

iex> ResponseHandler.handle({:ok, [1, 2, 3]})
"Got data: [1, 2, 3]"
iex> ResponseHandler.handle({:error, "not found"})
"Error: not found"
iex> ResponseHandler.handle(:skip)
"Skipping"
Enter fullscreen mode Exit fullscreen mode

Complex Pattern Matching

# Nested pattern matching
defmodule Temp do
  def process({:user, status, data}) when status in [:active, :inactive] do
    case {status, data} do
      {:active, %{name: name}} -> "Active user: #{name}"
      {:inactive, _} -> "Inactive user"
    end
  end
end

iex> Temp.process({:user, :active, %{name: "John"}})
"Active user: John"
iex> Temp.process({:user, :inactive, %{name: "John"}})
"Inactive user"


# Guards with atoms
defmodule Guard do
  def check(value) when is_atom(value), do: "Got atom: #{value}"
  def check(value), do: "Not an atom: #{inspect(value)}"
end

iex> Guard.check(:hello)
"Got atom: hello"
iex> Guard.check("hello")
"Not an atom: \"hello\""
Enter fullscreen mode Exit fullscreen mode

Common Atom Usage in Pattern Matching

Atoms are frequently used in pattern matching to express states, results, or commands:

Pattern Typical Use Case Example
:ok, :error Return tuples {:ok, result}
:pending State representation TaskState.next_state(:pending)
:true, :false Boolean-like operations if :true do ... end
:start, :stop Control commands {:start, pid}

Working with Booleans

In Elixir, true and false are actually atoms (:true and :false), making them consistent with Elixir's symbolic computation model.

Boolean Operations

# Basic operations
iex> true and false
false
iex> true or false
true
iex> not true
false

# Short-circuit operators
iex> false and raise("never reached")
false
iex> true or raise("never reached")
true

# Comparison operators
iex> 1 < 2
true
iex> "a" <= "b"
true
Enter fullscreen mode Exit fullscreen mode

Boolean Functions

# Functions returning booleans
# Run this code in IEx to test the functionality
defmodule Validator do
  # Guard clause ensures age is an integer
  def valid_age?(age) when is_integer(age), do: age >= 0 and age <= 150
  # Catch-all function for invalid types
  def valid_age?(_), do: false

  # Guard clause ensures email is a string (binary)
  def valid_email?(email) when is_binary(email) do
    String.contains?(email, "@") and String.contains?(email, ".")
  end
  def valid_email?(_), do: false
end

iex> Validator.valid_age?(25)
true
iex> Validator.valid_age?(-1)
false
iex> Validator.valid_email?("user@example.com")
true
Enter fullscreen mode Exit fullscreen mode

Control Flow with Booleans

# if/else expressions
iex> if true, do: "yes", else: "no"
"yes"

# unless expression
iex> unless false, do: "executed"
"executed"

# case with boolean conditions
case {true, false} do
 {true, _} -> "first is true"
 {_, true} -> "second is true"
 _ -> "both false"
end
"first is true"
Enter fullscreen mode Exit fullscreen mode

Understanding nil

nil Basics

# nil is an atom
iex> is_atom(nil)
true

# nil is falsy
iex> if nil, do: "true", else: "false"
"false"

# Testing for nil
iex> is_nil(nil)
true
iex> is_nil(:something)
false
Enter fullscreen mode Exit fullscreen mode

Working with nil

The nil value in Elixir represents the absence of a value. It is often used to signify "nothing here" or "not applicable."

Analogy: Imagine a blank field in a form where no input was provided—that's nil. Just like the field exists but has no value, a variable can exist but contain nil to represent the absence of data.

Examples

# Default values with nil
# The || operator returns the first value that isn't nil or false
iex> value = nil
iex> value || "default" # Useful for setting default values
"default"

# Maybe pattern
# Why use a Maybe module?
# Helps safely handle nil values and avoid runtime errors.
# Run this code in IEx to test the functionality
defmodule Maybe do
  # Implementation of Maybe/Optional pattern for safe nil handling
  # If value is nil, returns nil without executing the function
  def map(nil, _func), do: nil
  # If there's a value, applies the function
  def map(value, func), do: func.(value)
end

iex> Maybe.map(nil, &(&1 * 2))
nil
iex> Maybe.map(5, &(&1 * 2))
10

# Handling nil in data structures
iex> data = %{user: nil}
%{user: nil}
iex> get_in(data, [:user, :name])
nil
Enter fullscreen mode Exit fullscreen mode

Conclusion

Atoms, booleans, and nil are fundamental types in Elixir, forming the building blocks of symbolic computation and control flow. Through this guide, we’ve explored:

  • The core behavior of atoms, booleans, and nil
  • Common ways to use these types in pattern matching and state representation
  • Examples of their interaction in control flow

These basic types are essential for understanding Elixir’s functional paradigm and serve as a foundation for exploring advanced features such as concurrency, error handling, and process management.

Tip: Experiment with atoms, booleans, and nil in small projects or exercises to reinforce your understanding of their behavior and interactions.

Further Reading

Next Steps

In the upcoming article, we'll dive into Control Structures in Elixir:

Control Structures - If and Unless

  • Understanding conditional expressions
  • If and unless as expressions
  • Multiple conditions and else clauses
  • Guard clauses in conditionals

Top comments (2)

Collapse
 
leandrobighetti profile image
Leandro Bighetti

Loved the article! Especially the fundamental explanation of what an atom is, it’s actually challenging to explain that it’s a type that the value is its own name, and you explained it very directly and clearly! Probably one elf the best written explanations of it I’ve read! Love the examples as well, makes it easier to comprehend what this means in code.

I have 1 suggestion: I think it’s worth pointing explicitly to the reader that true and false are nothing but atoms themselves - :true and :false . An example like true = :true can perhaps illustrate this well. I think this would make your section using :true in the pattern matching clearer to understand as well for someone trying to first grasp the concept.

Overall was a really nice and entertaining read - even though I personally already knew what an atom was I still read the whole article because it was engaging and a good read. Keep up the great work ! ❤️

Collapse
 
abreujp profile image
João Paulo Abreu

Thank you so much for your kind words and detailed feedback! I'm really glad you found the article clear and engaging, especially the explanation of atoms. I've just updated the article to include your excellent suggestion, adding a note that explains how true and false are atoms (:true and :false) at the beginning of the Boolean section. Thank you for taking the time to read and provide such constructive feedback, even though you were already familiar with the concepts. Your input helps make the content more complete and clearer for everyone!