Immutable data is a foundation of abstraction - it localizes program computation. So immutable data makes concurrency easy & decreases bugs. All data in Erlang/Elixir is immutable. We are happy to use functional programming techniques, worrying about nothing on parallel programming.
To lookup in a nested data is easy. When there is a map below :
v = %{ x: [ {:ok, a: 1, b: 2}, {:ok, a: 3, b: 4}, ] }
Following codes are same.
1 = elem(Enum.at(v.x, 0), 1)[:a] 1 = elem(Enum.at(v[:x], 0), 1)[:a] 1 = (v.x |> Enum.at(0) |> elem(1))[:a] # We can pipe all. 1 = v |> Map.get(:x) |> Enum.at(0) |> elem(1) |> Keyword.get(:a) 1 = v |> Access.get(:x) |> Enum.at(0) |> elem(1) |> Access.get(:a) 1 = get_in(v.x, [Access.at(0), Access.elem(1)])[:a] 1 = get_in(v, [:x, Access.at(0), Access.elem(1), :a])
We can use pattern matching too.
1 = with %{x: [{:ok, a: a, b: _} | _]} = v, do: a 1 = case v, do: (%{x: [{:ok, a: a, b: _} | _]} -> a) true = match?(%{x: [{:ok, a: 1, b: _} | _]}, v)
But it's hard to UPDATE a nested data.
v_2 = Map.update!(v, :x, fn x -> List.update_at(x, 0, fn {:ok, k} -> {:ok, Keyword.update!(k, :a, &(&1 + 1))} end) end) 1 = elem(Enum.at(v.x, 0), 1)[:a] 2 = elem(Enum.at(v_2.x, 0), 1)[:a]
Why this's hard? The nested callback functions is complecated. It's not composable, so we can't split this into simple parts. Is there no way?
JavaScript - Immer
Let's see some examples in other languages.
Data in JavaScript isn't immutable. But after React & Redux comes, JavaScript loves immutability. Immer is a nice library to update nested data with immutability.
mweststrate/immer: Create the next immutable state by mutating the current one
import produce from "immer"; const v = { x: [ ["ok", { a: 1, b: 2 }], ["ok", { a: 3, b: 4 }], ], }; const v2 = produce(v, (v) => { v.x[0][1].a += 1; }); assert.equal(v.x[0][1].a, 1); assert.equal(v2.x[0][1].a, 2);
Data in JavaScript is mutable, so it has short syntax to update data. Immer use it. It Proxy the data, listen all mutation operations & create a new data. This's the way in a mutable language to update nested data with immutability.
Lens @ Haskell
Haskell is close to Elixir because data in Haskell is immutable. Haskell has a nice mechanism - Lens.
lens: Lenses, Folds and Traversals
Lens is a set of functions of composable getter & setter.
{-# LANGUAGE TemplateHaskell #-} import Lens.Micro import Lens.Micro.TH data Item = Item { _a :: Integer, _b :: Integer } deriving (Show) makeLenses ''Item data V = V { _x :: [(String, Item)] } deriving (Show) makeLenses ''V v = V { _x = [ ("ok", Item { _a = 1, _b = 2 }), ("ok", Item { _a = 3, _b = 4 }) ] } main = do print $ v ^?! to _x . ix 0 . _2 . to _a -- 1 print $ v ^?! x . ix 0 . _2 . a -- 1 print $ v & x . ix 0 . _2 . a .~ 2 -- V {_x = [("ok",Item {_a = 2, _b = 2}),("ok",Item {_a = 3, _b = 4})]} print $ v & x . ix 0 . _2 . a %~ (+ 1) -- V {_x = [("ok",Item {_a = 2, _b = 2}),("ok",Item {_a = 3, _b = 4})]} print $ v & (x . ix 0 . _2 . a) +~ 1 -- V {_x = [("ok",Item {_a = 2, _b = 2}),("ok",Item {_a = 3, _b = 4})]}
Lens is just a function, so we can compose them & create custom Lens (makeLenses
dose).
Clojure - a neighbor of Elixir
In ancient days Elixir was born from Clojure. So we look at that. Clojure is a functional language which is based on immutable data.
get-in
, assoc-in
& update-in
are the basic tools to manipulate nested data.
(def v {:x [["ok" {:a 1, :b 2}] ["ok" {:a 3, :b 4}]]}) ; get ; => 1 ((((v :x) 0) 1) :a) (:a (((:x v) 0) 1)) (:a (get (get (:x v) 0) 1)) (-> v :x (get 0) (get 1) :a) (get-in v [:x 0 1 :a]) (let [{:keys [x]} v [[_ {:keys [a]}]] x] a) ; assoc ; => {:x [["ok" {:a 2, :b 2}] ["ok" {:a 3, :b 4}]]} (update v :x (fn [x] (update x 0 (fn [i] (update i 1 (fn [j] (assoc j :a 2))))))) (assoc-in v [:x 0 1 :a] 2) ; update ; => {:x [["ok" {:a 2, :b 2}] ["ok" {:a 3, :b 4}]]} (update v :x (fn [x] (update x 0 (fn [i] (update i 1 (fn [j] (update j :a #(+ 1 %)))))))) (update-in v [:x 0 1 :a] #(+ 1 %))
Note assoc & update. Nested callbacks are transformed into a sequence of keys [:x 0 1 :a]
. The sequence is just a data, so we can compose them.
Elixir has Access
!
Like get-in
, assoc-in
& update-in
in Clojure, Elixir has get_in/2
, put_in/3
, update_in/3
& pop_in/2
.
v = %{x: [{:ok, a: 1, b: 2}, {:ok, a: 3, b: 4}]} 1 = get_in(v, [:x, Access.at(0), Access.elem(1), :a]) %{x: [{:ok, a: 2, b: 2}, {:ok, a: 3, b: 4}]} = put_in(v, [:x, Access.at(0), Access.elem(1), :a], 2) %{x: [{:ok, a: 2, b: 2}, {:ok, a: 3, b: 4}]} = update_in(v, [:x, Access.at(0), Access.elem(1), :a], &(&1 + 1)) {1, %{x: [{:ok, b: 2}, {:ok, a: 3, b: 4}]}} = pop_in(v, [:x, Access.at(0), Access.elem(1), :a])
These are family of Access module. These take a "path", a sequence of keys & functions. Map & Keyword takes a key. List needs Access.at/1
. Tuple needs Access.elem/1
.
%{x: 2} = put_in(%{x: 1}, [:x], 2) %{x: 2} = put_in(%{x: 1}[:x], 2) %{x: 2} = put_in(%{x: 1}.x, 2) [x: 2] = put_in([x: 1], [:x], 2) [x: 2] = put_in([x: 1][:x], 2) %{x: 1, y: 2} = put_in(%{x: 1}[:y], 2) [y: 2, x: 1] = put_in([x: 1][:y], 2) # `Access.key!/1` is also available for Map. %{x: 2} = put_in(%{x: 1}, [Access.key!(:x)], 2) try do put_in(%{x: 1}, [Access.key!(:y)], 2) rescue err in KeyError -> err end try do put_in(%{x: 1}.y, 2) rescue err in KeyError -> err end # `Access.key/2` provides a default value. This is useful for put_in & update_in. %{x: 1, y: %{z: 2}} = put_in(%{x: 1}, [Access.key(:y, %{}), :z], 2) # List [2] = put_in([1], [Access.at(0)], 2) nil = get_in([1], [Access.at(1)]) # Tuple {2} = put_in({1}, [Access.elem(0)], 2)
You'll find that accessing Struct causes an error. Struct isn't accessible in default. But we can use Access.key!/1
& Access.key/2
.
defmodule Example do defstruct x: 1 end defmodule Main do def main do %Example{x: 2} = put_in(%Example{}, [Access.key!(:x)], 2) %Example{x: 2} = put_in(%Example{}, [Access.key(:x)], 2) end end Main.main
One of the best feature of Access is Access.all/0
& Access.filter/1
. These reduce nested Enum calls. Access.all/0
traverses a List. Access.filter/1
traverses a List & filter them.
%{x: [%{a: 2}, %{a: 3}]} = update_in(%{x: [%{a: 1}, %{a: 2}]}.x, fn x -> for %{a: a} <- x, do: %{a: a + 1} end) %{x: [%{a: 2}, %{a: 3}]} = update_in(%{x: [%{a: 1}, %{a: 2}]}, [:x, Access.all(), :a], &(&1 + 1)) require Integer %{x: [%{a: 1}, %{a: 1}]} = update_in( %{x: [%{a: 1}, %{a: 2}]}.x, fn x -> x |> Enum.reduce( [], fn %{a: a}, accm when rem(a, 2) == 0 -> [%{a: div(a, 2)} | accm] i, accm -> [i | accm] end ) |> Enum.reverse end ) %{x: [%{a: 1}, %{a: 1}]} = update_in( %{x: [%{a: 1}, %{a: 2}]}, [:x, Access.filter(&Integer.is_even(&1.a)), :a], &div(&1, 2) )
Extend Access
There are 2 ways to extend Access.
- Create an Access fun
@impl Access
Create an Access fun
We can create functions like Access.*/*
by ourselves. It's called "access_fun".
access_fun(data, get_value) :: get_fun(data, get_value) | get_and_update_fun(data, get_value)
access_fun is an union of get_fun & get_and_update_fun.
get_fun(data, get_value) :: (:get, data, (term() -> term()) -> {get_value, new_data :: container()}) get_and_update_fun(data, get_value) :: (:get_and_update, data, (term() -> term()) -> {get_value, new_data :: container()} | :pop)
Let's create one. In a following situation,
v = %{x: [{:ok, a: 1, b: 2}, {:ok, a: 3, b: 4}]} 1 = get_in(v, [V.a(1), :a]) %{x: [{:ok, a: 2, b: 2}, {:ok, a: 3, b: 4}]} = put_in(v, [V.a(1), :a], 2) %{x: [{:ok, a: 2, b: 2}, {:ok, a: 3, b: 4}]} = update_in(v, [V.a(1), :a], &(&1 + 1))
the access_fun will be this.
defmodule V do def a(a), do: fn command, data, next -> a(command, data, a, next) end defp a(:get, data, a, next) do data.x |> Enum.find(fn {:ok, x} -> x[:a] == a _ -> false end) |> elem(1) |> next.() end defp a(:get_and_update, data, a, next) do {{:ok, x}, i} = data.x |> Enum.with_index |> Enum.find({{:ok, nil}, nil}, fn {{:ok, x}, _} -> x[:a] == a _ -> false end) case next.(x) do {get, update} -> data = put_in(data, [:x, Access.at(i), Access.elem(1)], update) {get, data} :pop -> if is_nil(i) do data else {_, data} = pop_in(data, [:x, Access.at(i), Access.elem(1)]) data end end end end
access_fun pros :
- It can traverse all data.
- Separate from other Access behaviour.
- You can write complex behaviour.
access_fun cons :
- It has many boilerplate.
- Too complex.
When you just want a short cut of Access path, you can implement a Access behaviour.
@impl Access
Access behaviour provides more fluent syntax for users. When a struct is, you should implement Access like this.
defmodule V do defstruct x: [] @behaviour Access @impl Access def fetch(v, key) do case path_to(v, key) do nil -> :error path -> {:ok, get_in(v, path)} end end @impl Access def get_and_update(v, key, fun) do case path_to(v, key) do nil -> {nil, v} path -> get_and_update_in(v, path, &fun.(&1)) end end @impl Access def pop(v, {_, _} = key) do case path_to(v, key) do nil -> {nil, v} path -> pop_in(v, path) end end defp path_to(v, {:a, a}) do with i when not is_nil(i) <- Enum.find_index(v.x, fn {:ok, x} -> x[:a] == a _ -> false end) do [Access.key!(:x), Access.at(i), Access.elem(1)] else _ -> nil end end end
Easy. Then you can call this like natively supported syntax.
defmodule Main do def main do v = %V{x: [{:ok, a: 1, b: 2}, {:ok, a: 3, b: 4}]} 1 = v[{:a, 1}][:a] %V{x: [{:ok, a: 2, b: 2}, {:ok, a: 3, b: 4}]} = put_in(v[{:a, 1}][:a], 2) %V{x: [{:ok, a: 2, b: 2}, {:ok, a: 3, b: 4}]} = update_in(v[{:a, 1}][:a], &(&1 + 1)) {1, %V{x: [{:ok, b: 2}, {:ok, a: 3, b: 4}]}} = pop_in(v, [{:a, 1}, :a]) end end Main.main
Access is composable & extensible abstraction to get & update deep nested data. Let's enjoy functional programming on Elixir!