Elixir collections

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.

CollectionExampleWhen
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 after Map.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


See also