Elixir is a function programming language that I have been using a lot in recent months to build all kinds of applications. Understanding of built-in collection types is essential to use any language effectively and Elixir is no different.
This posts summarizes all collection type along with pros/cons/gotchas for each one of them.
Collection | Example | When |
---|---|---|
Tuples | {:ok, "All good"} |
Returning data from a function |
Lists | [1, "two", :three] |
For a collection of items |
Keyword lists | [one: 1, two: 2] |
Passing options to a function |
Maps | %{one: 1, two: 2} |
Flexible key/value store |
Structs | %User{name: "John", age: 32} |
Typed/fixed key/value store |
Tuples
{:ok, foo}
“tagged” tuple since begins with an atom like:ok
or:error
like{:error, 543, "some error"}
Examples:
result = {:weather, 24, "montreal"}
temp = elem(result, 1) # 24
Updating tuple (rarely used):
put_elem(result, 1, temp + 2) # {:weather, 26, "montreal"}
pros
- Very fast since stored in contiguous memory.
- Great for pattern matching.
cons
- Have to know what each position holds.
- Not easy to update, coupling by position.
Lists
-
Collection of values.
-
Stored in multiple locations in memory (linked list).
-
Build with sigil (string values):
~w(one two three)
=>["one", "two", "three"]
-
Build with sigil (atom values):
~w(one two three)a
=>[:one, :two, :three]
-
Concatenation:
[1, 2, 3] ++ [4, 5]
=>[1, 2, 3, 4, 5]
-
[1, 2] - [2, 3]
=>[1]
-
When you really want a list but don’t know if a function would return a list or plain value:
List.wrap("Hello")
=>["Hello"]
List.wrap(["Hello"])
=>["Hello"]
-
[1 | [2, 3]]
=>[1, 2, 3]
-
hd ["one", "two"]
=>"one"
, will throw error if the list is empty -
List.first ["one", "two"]
will return nil if list is empty -
tl [1, 2, 3]
=>[2, 3]
, will throw error if the list is empty -
List.last [1, 2, 3]
=>3
, will return nil if the list is empty -
[head | tail] = [1, 2, 3]
=>head: 1, tail: [2, 3]
-
Enum.map([1, 2, 3], fn(i) -> i * 2 end)
=>[2, 4, 6]
-
Enum.reject(["one", nil, 3], &is_nil/1)
=>["one", 3]
-
for i <- [1, 2, 3], do: i * 2
=>[2, 4, 6]
-
for i <- [1, 2, 3], j <- [1, 2], do i * j
=>[1, 2, 2, 4, 3, 6]
-
Pattern matching
- Pattern based function definition
def hello([x, x, | _tail]), do: :twins def hello([head | _tail]) when is_number(head) do :number end def hello(_), do: :anything
- Calling this function
hello(["one"])
=>:anything
hello([1, "two", 3]
=>:number
hello([1, 1, 2, 3])
=>:twins
pros
- Easy to manipulate
- Fast when pushing on the list
cons
- Limited pattern matching in function heads (head and/or tail).
- Difficult to pattern match in the middle of list.
Keyword Lists
[foo: "bar", hello: "you", foo: "another bar"]
-
Associative collection (key/value)
-
Keys are atoms only, ordered, and not unique
- e.g. two :foo’s in above example
-
Implements Access behavior (list[key])
-
Not an erlang type (it’s elixir’s syntactic sugar).
-
Special list of 2-term tuples.
- So, above example exactly same as:
[{:foo, "bar"}, {:hello, "you"}, {:foo, "another bar"}]
- It’s not a hash, it’s just a list of 2-term tuples
-
list = [id: 1, title: "Post"]
list[:title]
=>"Post"
-
Enum.map(list, fn({k, v}) -> k end) # [:id, :title]
- This shows that keyword list is really just a list of 2-term tuples.
-
Keywords module:
- Keyword.take/2
- Keyword.has_key?/2
- Keyword.keys/1
- Keyword.values/1
- Keyword.put/3
- …
-
Mostly used for passing options to a function:
def say(message, options \ []) do prefix = Keyword.get(options, :prefix, Time.utc_now) "#{prefix} #{message}" end say("World") => "14:19:53.464950 World" say("World", prefix: "Hello") => "Hello World"
pros
- Awesome for passing options to a function.
- Can use all operations available to lists.
cons
- Like a regular list, we can’t pattern match on one element of a list containing multiple elements.
Maps
%{id: 1234, title: "Maps rock!"}
-
Associative collection (key/value)
-
“The” key-value store in elixir
-
Keys can be in any order and of any type (unlike keyword lists where keys are atoms only)
-
Keys must be unique
-
Can pattern match on a single element
-
Implements access behavior
-
Closed thing to hash or dict in other languages
-
Access
map = %{id: 1234, title: "Maps rock!"} %{id: id} = map id # 1234 map.title <> map[:title] # "Maps rock!Maps rock!"
-
Merge
post = %{id: 1234, title: "Maps rock!"} %{post | title: "MAPS ROCK!"} # NOTE: returns a new map, original post map is not mutated %{post | what: "MAPS ROCK!"}` # **(KeyError) key :what not found # So, how to add something to map? Map.put(post, :what, "Boo") # %{id: 1234, title: "Maps rock!", what: "Boo"}
-
Pattern matching
def square?(%{width: x, height: x}), do: true def square?(_), do: false
-
Map module
Map.take/2 Map.has_key?/2 Map.keys/1 Map.values/1 Map.put/3 ...
pros
- Awesome for passing data to a function (while keyword lists are great for passing options to a function).
- Great for pattern matching in function heads.
cons
- Unstructured and untyped data store.
- No indifferent access (if key is an atom, you can’t access it’s value using a string).
Structs
%Post{id: 1234, title: "Structs rock!"}
-
Extension built on top of a map (not in Erlang).
-
Compile time guarantees that only the fields defined through defstruct will be allowed in the struct.
defmodule User do defstruct name: "John", age: 27 end user = %User{name: "Tim"} # %User{age: 27, name: "Tim"} IO.inspect user # %User{age: 27, name: "Tim"} IO.inspect user, structs: false # destroy the notion of structs # %{__struct__: User, age: 27, name: "John"}
-
The last
IO.inspect
example shows that a struct is really just a map with a special key (struct) to tell elixir what kind of struct it is.use = %User{name: "Tim"} # %User{age: 27, name: "Tim"} %{user | id: 1} # ** (KeyError) key :id not found
NOTE:
Map.put(user, id: 1)
# %{__struct__: User, age: 27, id: 1, name: "Tim"}
Map.put
does not return a struct but a raw map. So, we sort of lose the original struct afterMap.put
pros
- All the advantages of a map
- Type checking enforced at compile time
cons
- Be careful when using Map functions, you could end-up with a struct disconnected from its definition, if you add keys that are not in the struct.
References
- Collections in Elixir - YouTube
- Designing Elixir Systems with OTP (by James Edward Gray, II and Bruce A. Tate), Chapter 2 - Know your Elixir Datatypes