Basic Types Overview

There aren’t that many basic data types in Elixir. These are the ones we’ll cover here.

  • atom
  • boolean
  • nil
  • integer
  • float
  • string
Thinking Tip: Learn by doing

Remember, we learn best by doing. Open an IEx shell and play with each of these types as we cover them.

Atom

An atom is a literal, a constant with name. It is a constant whose name is its value. Atoms are very useful in pattern matching and are used for more than you might expect in Elixir.

Examples of Atoms:

:my_atom
:testing
:customer_type

An atom begins with a colon and typically is all lowercase letters using an underscore to separate words.

If you need an atom to contain spaces or special characters that otherwise wouldn’t be valid, you can express it as a string with the colon in front.

:"I'm still an atom"

However, writing atoms this way is unusual and only done when needed for specific exceptional instances.

An Atom in Elixir is similar to a symbol in Ruby or an enumeration in C/C++. Atoms are stored in the “atom table”. All references to an atom like “:ok” are shared and actually all point to the same atom table entry.

Functions for working with an Atom can be found in the Atom module.

Preventing Denial-of-Service

Atoms are not garbage collected. The important thing to learn from this is you should not allow user provided content to become an atom at run-time in your system. Converting unchecked user data to an atom can expose your system to a Denial of Service (DOS) attack. The attack works like this, a malicious actor causes your system to repeatedly create unique atoms until it consumes all of the available resources on your machine ultimately causing it to crash. It isn’t a security flaw, it isn’t a “break-in”, but it can be abused to cause your system to crash.

You can allow user input to become an atom using the String.to_existing_atom/1. If you already have an atom defined in the system, it accepts the conversion. If you try to convert to something new, it raises an exception blocking the operation.

iex(1)> String.to_existing_atom("ok")
:ok

iex(2)> String.to_existing_atom("whaaaat")
** (ArgumentError) argument error
    :erlang.binary_to_existing_atom("whaaaat", :utf8)

Boolean

Examples are: true and false.

The boolean values are actually implemented as special reserved atoms.

false == :false
#=> true

true == :true
#=> true

Nil

Nil represents the absence of a value. It is like null in Javascript and many other languages.

Interestingly, nil is also implemented as a reserved atom.

nil == :nil
#=> true

In evaluations, nil behaves like false.

if nil || true, do: "True!", else: "False."
#=> "True!"

Integer

Represents positive and negative whole numbers. Including 0.

Functions for working with an Integer can be found in the Integer module.

Examples:

-10, 0, 1000

Integers can be arbitrarily large. It uses as much data is needed to contain it.

99999999999999999999999999999999999999999999999999999999

Can be represent as Hex and more.

0xFF
#=> 255

Integers can use _ as a grouping separator. This is functionally ignored but helps with readability.

1_000_000
#=> 1000000

Float

Represents floating point numbers.

Examples:

-12.4, 0.5, 100.67, 83.33333333333

Functions for working with a Float can be found in the Float module.

Floats don’t support a “Decimal” type for explicit levels of precision. As such, it results in typical floating point rounding issues.

0.8 * 3 
#=> 2.4000000000000004

This is expected with floating point math. For further explanation, see these resources:

If you need to represent fixed decimal values for something like money, using a Float may not be the most appropriate choice. For example, it may work better to use an integer that represents cents (instead of whole dollars).

Another option is to use a community package called Decimal that provides arbitrary decimal precision.

Scientific Notation

Floats are frequently displayed in their scientific notation. This may not be what you expect or want.

1200.0
#=> 1.2e3

Note that when working in IEx, it converts the data to a string for display in the shell. This is the same display as Float.to_string/1.

Float.to_string(1200.0)
#=> "1.2e3"

If you need to convert a float to a string with explicit decimal precision, use the built-in Erlang function float_to_binary. In Elixir, you can use any Erlang function. In this example, the function we want is declared in the erlang module. To call it, use an atom as the module name like this:

:erlang.float_to_binary(1200.0, decimals: 2)
#=> "1200.00"

String

Strings are encoded in UTF-8. A string uses the double quote character ". Single quoted text is not a string, that’s a charlist and behaves differently.

"The quick brown fox"

The String module has a number of functions for working with them.

String.upcase("hello world")
#=> "HELLO WORLD"

Concatenation

Elixir strings can be concatenated using the <> operator.

"one" <> " two"
#=> "one two"

text = "Hey"
text <> " friends!"
#=> "Hey friends!"

Interpolation

Elixir strings support interpolation using the #{...} characters embedded in a string.

name = "Tom"
"Greetings #{name}!"
#=> "Greetings Tom!"

Strings are Binary

UTF-8 strings are a more recent addition to Erlang. Because of this, many Erlang functions that take a string don’t take an Elixir string. Erlang sees an Elixir string as a binary type.

is_binary("a string")
#=> true

In Erlang, functions often expect a charlist.

Charlist

Mostly used for interoperability with Erlang. It is just a list of code points and is created with single quotes.

'this is a charlist'
#=> 'this is a charlist'

is_list('testing')
#=> true

A charlist can be converted to a string and vise versa using these functions:

“Kernel” is Elixir’s default environment. It even defines basic math operators like + and -. Because it is so essential to Elixir, the Kernel module is always included for you. You can just use the functions to_string/1 and to_charlist/1 to convert between the types without needing to specify the “Kernel” module explicitly.

to_string('hello world')
#=> "hello world"

to_charlist("hello world")
#=> 'hello world'

Modules to Manipulate

All of these types are primitives. They are “data”. They are not objects with functions attached to them. There are no “objects” in Elixir. There is only “data” and “functions”. Modules are a collection or a container for functions.

By convention, when you want to perform some function on a piece of data, you use the type’s module for doing that. This is particularly the case for Atom, Integer, Float, and String.

4 Comments

  1. Maksym Kosenko on October 3, 2021 at 1:42 pm

    This lesson is short,
    But it explains a lot! )

  2. Larry Rix on September 29, 2022 at 3:06 pm

    “An atom is a literal, a constant with name. It is a constant whose name is its value.”

    Best explanation of an Atom that I have seen so far!

  3. Venkat Poojari on November 16, 2022 at 2:18 am

    An Open question, When should we go for Charlist vs String?

    • Mark Ericksen on November 16, 2022 at 7:53 am

      Hi Venkat!

      You almost certainly never want a charlist in your Elixir code. The only times I can think of are:
      – you are working directly with an Erlang library that takes a charlist. If so, work with strings and convert it when passing.
      – you are working with a file format (or binary format) that best fits a charlist. Again, I’d convert it to work with the data though.

      Hope that helps!

Leave a Comment

You must be logged in to post a comment.