Elixir In Action - Part 1 - Functional Elixir
defmodule NotebookHelpers do
def directory(), do: (__ENV__.file |> Path.dirname())
def file_path(filename), do: directory() <> "/files/" <> filename
end
4.1.1 Basic Abstractions
defmodule TodoList do
@moduledoc """
## Examples
iex> todo_list = TodoList.new() |>
...>TodoList.add_entry(~D[2023-12-19], "Dentist") |>
...>TodoList.add_entry(~D[2023-12-20], "Shopping") |>
...>TodoList.add_entry(~D[2023-12-19], "Movies")
iex> TodoList.entries(todo_list, ~D[2023-12-19])
["Movies", "Dentist"]
iex> TodoList.entries(todo_list, ~D[2023-12-18])
[]
"""
def new(), do: %{}
def add_entry(todo_list, date, title) do
Map.update(todo_list, date, [title], &([title | &1]))
end
def entries(todo_list, date), do: Map.get(todo_list, date, [])
end
4.1.2 Composing Abstractions
defmodule MultiDict do
def new(), do: %{}
def add(dict, key, value) do
Map.update(dict, key, [value], &([value | &1]))
end
def get(dict, key) do
Map.get(dict, key, [])
end
end
defmodule TodoList2 do
@moduledoc """
## Examples
iex> alias TodoList2, as: TodoList
iex> todo_list = TodoList.new() |>
...>TodoList.add_entry(~D[2023-12-19], "Dentist") |>
...>TodoList.add_entry(~D[2023-12-20], "Shopping") |>
...>TodoList.add_entry(~D[2023-12-19], "Movies")
iex> TodoList.entries(todo_list, ~D[2023-12-19])
["Movies", "Dentist"]
iex> TodoList.entries(todo_list, ~D[2023-12-18])
[]
"""
def new(), do: MultiDict.new()
def add_entry(todo_list, date, title), do: MultiDict.add(todo_list, date, title)
def entries(todo_list, date), do: MultiDict.get(todo_list, date)
end
alias TodoList2, as: TodoList
4.1.3 Structuring data with maps
defmodule TodoList3 do
@moduledoc """
## Examples
iex> alias TodoList3, as: TodoList
iex> todo_list = TodoList.new() |>
...>TodoList.add_entry(%{date: ~D[2023-12-19],title: "Dentist"}) |>
...>TodoList.add_entry(%{date: ~D[2023-12-20],title: "Shopping"}) |>
...>TodoList.add_entry(%{date: ~D[2023-12-19],title: "Movies"})
iex> TodoList.entries(todo_list, ~D[2023-12-19])
[
%{date: ~D[2023-12-19], title: "Movies"},
%{date: ~D[2023-12-19], title: "Dentist"}
]
iex> TodoList.entries(todo_list, ~D[2023-12-18])
[]
"""
def new(), do: MultiDict.new()
def add_entry(todo_list, entry), do: MultiDict.add(todo_list, entry.date, entry)
def entries(todo_list, date), do: MultiDict.get(todo_list, date)
end
alias TodoList3, as: TodoList
4.2.1 Generating Ids (with a twist)
defmodule Entry do
defstruct [:id, :date, :title]
def new(id, date, title), do: %Entry{id: id, date: date, title: title}
end
defmodule TodoList4 do
@moduledoc """
## Examples
iex> alias TodoList4, as: TodoList
iex> todo_list = TodoList.new() |>
...>TodoList.add_entry(%{date: ~D[2023-12-19], title: "Dentist"}) |>
...>TodoList.add_entry(%{date: ~D[2023-12-20], title: "Shopping"}) |>
...>TodoList.add_entry(%{date: ~D[2023-12-19], title: "Movies"})
iex> TodoList.entries(todo_list, ~D[2023-12-19])
[
%Entry{date: ~D[2023-12-19], id: 1, title: "Dentist"},
%Entry{date: ~D[2023-12-19], id: 3, title: "Movies"}
]
iex> TodoList.entries(todo_list, ~D[2023-12-18])
[]
"""
defstruct next_id: 1, entries: %{}
def new(), do: %TodoList4{}
def add_entry(%TodoList4{} = todo_list, %{date: date, title: title}) do
entry = Entry.new(todo_list.next_id, date, title)
entries = Map.put(todo_list.entries, todo_list.next_id, entry)
%TodoList4{todo_list | next_id: entry.id + 1, entries: entries}
end
def entries(todo_list, date) do
Map.values(todo_list.entries)
|> Enum.filter(&(&1.date == date))
end
end
alias TodoList4, as: TodoList
4.2.2 Updating Entries
defmodule TodoList5 do
@moduledoc """
## Examples
iex> alias TodoList5, as: TodoList
iex> todo_list = TodoList.new() |>
...> TodoList.add_entry(%{date: ~D[2023-12-19], title: "Dentist"}) |>
...> TodoList.update_entry(1, fn entry -> %Entry{id: entry.id, date: ~D[2023-12-20], title: "UpdatedMovies"} end) |>
...> TodoList.update_entry(30, &(&1))
iex> TodoList.entries(todo_list, ~D[2023-12-20])
[
%Entry{date: ~D[2023-12-20], id: 1, title: "UpdatedMovies"},
]
iex> TodoList.entries(todo_list, ~D[2023-12-19])
[]
"""
defstruct next_id: 1, entries: %{}
def new(), do: %TodoList5{}
def add_entry(%TodoList5{} = todo_list, %{date: date, title: title}) do
entry = Entry.new(todo_list.next_id, date, title)
entries = Map.put(todo_list.entries, todo_list.next_id, entry)
%TodoList5{todo_list | next_id: entry.id + 1, entries: entries}
end
def entries(%TodoList5{} = todo_list, date) do
Map.values(todo_list.entries)
|> Enum.filter(&(&1.date == date))
end
def update_entry(%TodoList5{} = todo_list, entry_id, updater_fun) do
case Map.fetch(todo_list.entries, entry_id) do
:error -> todo_list
{:ok, entry} ->
updated_entry = updater_fun.(entry)
updated_entries = %{todo_list.entries | entry_id => updated_entry}
%TodoList5{todo_list | entries: updated_entries}
end
end
end
alias TodoList5, as: TodoList
4.2.3 Immutable hierarchical updates
defmodule TodoList6 do
@moduledoc """
## Examples
iex> alias TodoList6, as: TodoList
iex> todo_list = TodoList.new() |>
...> TodoList.add_entry(%{date: ~D[2023-12-19], title: "Dentist"}) |>
...> TodoList.update_entry(1, fn entry -> %Entry{id: entry.id, date: ~D[2023-12-20], title: "UpdatedMovies"} end) |>
...> TodoList.update_entry(30, &(&1))
iex> TodoList.entries(todo_list, ~D[2023-12-20])
[
%Entry{date: ~D[2023-12-20], id: 1, title: "UpdatedMovies"},
]
iex> TodoList.entries(todo_list, ~D[2023-12-19])
[]
iex> TodoList.delete_entry(todo_list, 1) |> TodoList.entries(~D[2023-12-20])
[]
"""
defstruct next_id: 1, entries: %{}
def new(), do: %TodoList6{}
def add_entry(%TodoList6{} = todo_list, %{date: date, title: title}) do
entry = Entry.new(todo_list.next_id, date, title)
entries = Map.put(todo_list.entries, todo_list.next_id, entry)
%TodoList6{todo_list | next_id: entry.id + 1, entries: entries}
end
def entries(%TodoList6{} = todo_list, date) do
Map.values(todo_list.entries)
|> Enum.filter(&(&1.date == date))
end
def update_entry(%TodoList6{} = todo_list, entry_id, updater_fun) do
case Map.fetch(todo_list.entries, entry_id) do
:error -> todo_list
{:ok, entry} ->
put_in(todo_list.entries[entry_id], updater_fun.(entry))
end
end
def delete_entry(%TodoList6{} = todo_list, entry_id) do
updated_entries = todo_list.entries |> Map.delete(entry_id)
%TodoList6{todo_list | entries: updated_entries}
end
end
alias TodoList6, as: TodoList
4.2.4 Iterative updates
defmodule TodoList7 do
@moduledoc """
## Examples
iex> alias TodoList7, as: TodoList
iex> entries = [
...> %{date: ~D[2023-12-19], title: "Dentist"},
...> %{date: ~D[2023-12-20], title: "Shopping"},
...> %{date: ~D[2023-12-19], title: "Movies"},
...> ]
iex> TodoList.new(entries)
%TodoList7 {
next_id: 4,
entries: %{
1 => %Entry{id: 1, date: ~D[2023-12-19], title: "Dentist"},
2 => %Entry{id: 2, date: ~D[2023-12-20], title: "Shopping"},
3 => %Entry{id: 3, date: ~D[2023-12-19], title: "Movies"}
}
}
"""
defstruct next_id: 1, entries: %{}
def new(entries \\ []) do
Enum.reduce(entries, %TodoList7{}, &(add_entry(&2, &1)))
end
def add_entry(%TodoList7{} = todo_list, %{date: date, title: title}) do
entry = Entry.new(todo_list.next_id, date, title)
entries = Map.put(todo_list.entries, todo_list.next_id, entry)
%TodoList7{todo_list | next_id: entry.id + 1, entries: entries}
end
def entries(%TodoList7{} = todo_list, date) do
Map.values(todo_list.entries)
|> Enum.filter(&(&1.date == date))
end
def update_entry(%TodoList7{} = todo_list, entry_id, updater_fun) do
case Map.fetch(todo_list.entries, entry_id) do
:error -> todo_list
{:ok, entry} ->
put_in(todo_list.entries[entry_id], updater_fun.(entry))
end
end
def delete_entry(%TodoList7{} = todo_list, entry_id) do
updated_entries = todo_list.entries |> Map.delete(entry_id)
%TodoList7{todo_list | entries: updated_entries}
end
end
alias TodoList7, as: TodoList
4.2.5 Exercise: Importing from a file
defmodule TodoList7.CsvImporter do
@moduledoc """
## Examples
iex> alias TodoList7, as: TodoList
iex> TodoList.CsvImporter.import("todos.csv")
%TodoList7{
next_id: 4,
entries: %{
1 => %Entry{id: 1, date: ~D[2023-12-19], title: "Dentist"},
2 => %Entry{id: 2, date: ~D[2023-12-20], title: "Shopping"},
3 => %Entry{id: 3, date: ~D[2023-12-19], title: "Movies"}
}
}
"""
def import(file_name) do
NotebookHelpers.file_path(file_name)
|> File.stream!(:line)
|> Stream.map(&line_to_entry/1)
|> TodoList.new()
end
defp line_to_entry(line) do
[date_string, title] = String.trim(line)
|> String.split(",", trim: true)
date = Date.from_iso8601!(date_string)
Map.new([{:date, date}, {:title, title}] )
end
end
TodoList.CsvImporter.import("todos.csv")
4.3.3 Built-in protocols
defimpl Collectable, for: TodoList7 do
@moduledoc """
## Examples
iex> alias TodoList7, as: TodoList
iex> todo_list = TodoList.CsvImporter.import("todos.csv")
iex> Enum.into([%{date: ~D[2023-12-21], title: "MyBook"}], todo_list)
%TodoList7{
next_id: 5,
entries: %{
1 => %Entry{id: 1, date: ~D[2023-12-19], title: "Dentist"},
2 => %Entry{id: 2, date: ~D[2023-12-20], title: "Shopping"},
3 => %Entry{id: 3, date: ~D[2023-12-19], title: "Movies"},
4 => %Entry{id: 4, date: ~D[2023-12-21], title: "MyBook"},
}
}
"""
def into(todo_list) do
fun = fn
todo_list_acc, {:cont, entry} -> TodoList7.add_entry(todo_list_acc, entry)
todo_list_acc, :done -> todo_list_acc
_, :halt -> :ok
end
{todo_list, fun}
end
end