[ immutable | elixir ]

Julian Doherty | @madlep | madlep.com

immutable

means data can't change

basic types


1 + 2 # 1 is still 1

true || false # true is still true
          

They're immutable

Nothing controversial there

lists

Also immutable in elixir


my_list = [:foo, :bar]
my_list.unshift(:baz) # NOT A THING. Won't work
          

Do this instead


my_list1 = [:foo, bar]
my_list2 = [:baz | my_list1]
# my_list1 == [:foo, :bar] (still)
# my_list2 == [:baz, :foo, :bar]
            

tuples

Yup. Immutable


my_tuple1 = {:person, "Kevin"}
my_tuple2 = put_elem(my_tuple, 1, "Bob")
# my_tuple1 == {:person, "Kevin") (still)
# my_tuple2 == {:person, "Bob")
          

maps and structs


my_map1 = %{name: "Stuart", colour: "yellow"}
my_map2 = %{my_map1 | name: "Bob"}
# my_map1 == %{name: "Stuart", colour: "yellow"}
# my_map2 == %{name: "Bob", colour: "yellow"}
          

WHY WOULD YOU DO THAT!?

State is the enemy. So make state explicit.

Easier to reason. Less to worry and think about.

Errors can be recovered. Just go back in time.

Let's go deeper

Lists

Can only prepend (easily)


# [ | ] syntax to prepend to a list
new_list = [new_element | old_list]

# [ | ] syntax to also pattern match to a list
[head | tail] = new_list
# head == new_element
# tail == old_list
          

Why only prepend to lists?

Elixir lists are cons lists

Single linked lists. Head contains element, and pointer to another list

cons list

[42 | [69 | [613] ] == [42, 69, 613]

          
(image from wikipedia)

Expensive to append to a list

prepend, then reverse instead


# SLOW. DON'T DO THIS
list = []
list = List.insert_at(list, -1, "Kevin")
list = List.insert_at(list, -1, "Stuart")
list = List.insert_at(list, -1, "Bob")

# Do this instead - fast. syntax sugar guides you
list = []
list = ["Kevin" | list]
list = ["Stuart" | list]
list = ["Bob" | list]
list = Enum.reverse(list)
          

Recursion

Don't write inline code with [ | ] over and over

Can't do traditional loops (for needs to mutate something)

So we use recursive functions work on lists


def sum(list) do
  sum(list, 0)
end

def sum([], acc) do
  acc
end

def sum([h|t], acc) do
  sum(t, h + acc)
end
          

Enum does recursion for you


Enum.reduce(my_list, 0, fn(n, acc) -> n + acc end)

# OR

Enum.sum(my_list)
          

Maps / Structs

Implemented as a persistent data structure. Shallow copy on modification. Nodes point to data as before

Single level changes are easy


my_map = %{name: "Kevin", colour: "Yellow"}
my_map2 = %{my_map | name: "Evil Kevin", colour: "Purple"}
          

Hierarchical maps are tricker

use put_in / get_in / update_in etc


put_in(my_complex_map[:minions][:bob][:name], "Kevin")
          

limitation - doesn't work for mixed maps/lists/tuples. only maps

State is explicit

Hopefully you've seen by now that elixir makes it obvious and explicit when you are going to mutate state.

You need to think about this, instead of it "just happening"

State managemnent

You can roll your own state management (like we've seen)

Or use built in functionality:

  • Agent
  • GenServer
  • ets/dets/mnesia
  • files
  • database
  • whatever
  • process dictionary
    (don't use process dictionary)

Usually, processes own state in elixir

Actor pattern is commonly used

Receive block allows explicit control by actor over how state changes get applied

About a million tutorials on that already ;)

Thank you

Questions?

Julian Doherty | @madlep | madlep.com