collection_types

Explore Elixir

Elixir Collection Types

Understanding Elixir Collection Types

Elixir provides a rich set of collection types to manage data efficiently. Each type has its own strengths and is suited for different use cases. This guide explores the fundamental collection types in Elixir, detailing their syntax, common operations, and performance characteristics.

List

Lists are fundamental in Elixir, implemented as singly linked lists. They can hold elements of any type and are enclosed in square brackets [] with elements separated by commas. Lists are efficient for sequential access, especially when adding or removing elements from the head. For operations involving the tail, it's often more performant to add to the head and then reverse the list. Lists implement the Enumerable protocol, allowing for extensive manipulation via the Enum module.

Key operations include concatenation with ++, subtraction with --, and construction using the cons operator |. Functions like hd (head) and tl (tail) provide access to the first element and the rest of the list, respectively.

> [1, 2, 3.4, "a", "b", :c, [:d]]
> [ 1 | [2 | [3]]] == [1, 2, 3]   # true
> [1, 2, 3.4] ++ ["a", "b", :c]   # [1, 2, 3.4, "a", "b", :c]
> [1, 2, 3.4, "a", "b", :c, [:d]] -- [2, "a", "c"]  # [1, 3.4, "b", :c, [:d]]
> hd [1, 2, 3]               # 1
> tl [1, 2, 3]               # [2, 3]
> length [:a, :b, :c, :d]    # 4
> Enum.reverse [:a, :b, :c]  # [:c, :b, :a]
> Enum.member? [:a, :b], :b  # true
> Enum.join [:a, :b], "_"    # "a_b"
> Enum.at [:a, :b, :c], 1    # :b

Charlist

A Charlist is essentially a List of UTF-8 codepoints. Syntactically, they are distinguished by single quotes '. Despite the different syntax, CharLists are identical to Lists in their underlying structure and behavior. They support multi-line definitions and utilize the same Escape Sequences as Strings.

> 'char list'
> [108, 105, 115, 116] == 'list'  # true
> 'turbo' ++ 'pogo'               # 'turbopogo'
> 'char list' -- 'a l'            # 'christ'
> hd 'such list' == ?s            # true
> String.to_char_list "tacosalad" # 'tacosalad'
> List.to_string 'frijoles'       # "frijoles"
> [?Y, ?e, ?a, ?h] == 'Yeah'      # true

Tuple

Tuples are fixed-size collections that can store elements of any type. Their elements are stored contiguously in memory, making them efficient for index-based access. Tuples are defined using curly braces {} with comma-separated elements. While fast for direct element retrieval by index using elem/2, they are less efficient for operations involving a large number of elements compared to Lists.

> { :a, 1, {:b}, [2]}
> put_elem({:a}, 0, :b) # {:b}
> elem({:a, :b, :c}, 1) # b
> Tuple.delete_at({:a, :b, :c}, 1) # {:a, :c}
> Tuple.insert_at({:a, :c}, 1, :b) # {:a, :b, :c}
> Tuple.to_list({:a, :b, :c})      # [:a, :b, :c]

Keyword List

A Keyword List is a specialized List containing 2-element Tuples, where the first element of each Tuple is an Atom (the keyword or key). They offer a concise syntax, omitting the Tuple brackets and placing the key's colon on the right (e.g., [key: value]). Like Lists, Keyword Lists are ordered, can contain duplicate keys, and are efficient for head-based access. They support concatenation with ++ and subtraction with --. Elements can be accessed using [:key] notation, returning the first matching element. Equality comparison requires elements to be identical and in the same order.

# Full Syntax
> [{:a, "one"}, {:b, 2}]
# Concice Syntax
> [a: "one", b: 2]
> [a: 1] ++ [a: 2, b: 3] == [a: 1, a: 2, b: 3] # true
> [a: 1, b: 2] == [b: 2, a: 1]         # false! elements are in different order
> [a: 1, a: 2][:a] == 1                # true
> Keyword.keys([a: 1, b: 2])           # [:a, :b]
> Keyword.get_values([a: 1, a: 2], :a) # [1, 2]
> Keyword.keyword?([{:a,1}, {:b,2}])   # true
> Keyword.keyword?([{:a,1}, {"b",2}])  # false! "b" is not an Atom
> Keyword.delete([a: 1, b: 2], :a)     # [b: 2]

Map

Maps are key-value stores where keys and values can be of any type. Unlike Keyword Lists, Maps do not allow duplicate keys and are unordered. They are defined using %{ } with key-value pairs separated by =>. If all keys are Atoms, a shorthand syntax is available where the => is omitted and the Atom's colon is on the right (e.g., %{atom: value}). Values are accessed using [key] notation, and if the key is an Atom, dot notation (.key) can also be used. Maps are highly efficient for key-based lookups.

> %{:a => 1, 1 => ["list"], [2,3,4] => {"a", "b"}}
> %{:a => 1, :b => 2} == %{a: 1, b: 2}              # true
> %{a: "one", b: "two", a: 1} == %{a: 1, b: "two"}  # true
> %{a: "one", b: "two"} == %{b: "two", a: "one"}    # true
> %{a: "one", b: "two"}[:b]                         # "two"
> %{a: "one", b: "two"}.b                           # "two"
> %{a: "one", a: 1} == %{a: 1}                      # true
> %{:a => "one", "a" => "two"}."a" == "two"         # false! watchout
> Map.keys( %{a: 1, b: 2} ) == [:a, :b]             # true
> %{ %{a: 1, b: 2, c: 3} | :a => 4, b: 5 }          # %{a: 4, b: 5, c: 3}
> Map.merge( %{a: 1, b: 2}, %{a: 4, c: 3} )         # %{a: 4, b: 2, c: 3}
> Map.put( %{a: 1}, :b, 2 ) == %{a: 1, b: 2}        # true
> Kernel.get_in # TODO
> Kernel.put_in # TODO

Struct

Structs are essentially Maps with predefined keys, default values, and Atom keys. They are defined within a Module and take the Module's name. Structs do not implement the Access or Enumerable protocols, behaving like bare Maps. A special field, __struct__, holds the name of the struct.

defmodule City do
  defstruct name: "New Orleans", founded: 1718
end
nola = %City{}
chi =  %City{name: "Chicago", founded: 1833}
nola.name   # "New Orleans"
chi.founded # 1833
nola.__struct__ # City

Range

Ranges represent a sequence of numbers or other ordered elements, defined by a first and last element. They are a type of Struct with first and last fields. Ranges can be created using the special .. syntax or by explicitly constructing a Range struct. They are enumerable and can be iterated over using functions from the Enum module.

> a = 5..10
> b = Range.new(5, 10)
> c = %Range{first: 5, last: 10}
> Range.range?(c)   # true
> Enum.each(5..10, fn(n) -> n*n end) # prints all the squares of 5..10
> Map.keys(5..10)   # [:__struct__, :first, :last]
> (5..10).first     # 5

Streams

Streams are lazy enumerables, meaning their elements are not computed until explicitly requested by an Enum module function. They are created using functions within the Stream module. This lazy evaluation is particularly useful for handling potentially infinite sequences or large datasets, as it avoids unnecessary computation and memory usage.

The Stream.unfold/2 function is a powerful tool for creating custom streams, allowing for arbitrary stream generation based on an initial state and a function that produces the next element and the next state.

> a = Stream.cycle 'abc'
#Function<47.29647706/2 in Stream.unfold/2> # Infinate Stream created
> Enum.take a, 10                           # Enum.take computes the 10 elements
'abcabcabca'

With Stream.unfold/2 you can create an arbitrary stream.

> s = Stream.unfold( 5, 
  fn 0 -> nil            # returning nil halts the stream
     n -> {n, n-1}       # return format {next-val, rest}
  end)
> Enum.to_list(s)
[5, 4, 3, 2, 1]