Learning Elixir I
I really like the new concepts that I came across while learning Elixir programming language. I have a Python/ruby background and Elixir was the first flavor of functional language that I learned. I would like to list all my learnings over here so that this can be the article I can refer to if I want to quickly read up on elixir basics.
And, I will be taking the approach of learning by example so that it is very clear and easy to understand and remember.
Recursion
I am writing a few variances of recursion over here. This will primarily help in understanding:
- How to convert an iterative solution to a recursive one.
- How to write an efficient recursive function
To understand how to convert for
loop into the recursive function
Problem - Iterate over the first 100 numbers and calculate their sum:
defmodule Mod do
def _calc_sum([head | tail], acc), do: _calc_sum(tail, acc + head)
def _calc_sum([], acc), do: acc
def calc_sum() do
nums = Enum.to_list 0..100
_calc_sum(nums, 0)
end
end
Steps of conversion:
- Create a separate function for
for
loop. (In the above example, it is_calc_sum
) - One of the arguments in the function is the list that needs to be iterated
- All the other variables that remain common across the for loop should be taken as the rest of the arguments in the function.
Approach 2:
Problem - Given a list of strings, return a list of its uppercase version
defmodule Test do
def _to_upcase([head | tail]), do: [String.upcase(head) | _to_upcase(tail)]
def _to_upcase([]), do: []
def to_upcase(llist) do
_to_upcase(llist)
end
end
To understand how to iterate a list recursively in elixir
There are two ways to iterate a list in elixir:
defmodule Test do
@doc """
- Preserves the order of the list
- No additional variable required
"""
def approach_1([head | tail], func), do: [func.(head) | approach_1(tail)]
def approach_1([], _func), do: []
@doc """
- Reverse the resultant list
- Additional variable required
- Additional function that takes the additional variable required
"""
def approach_2(list, func), do: _approach_2(list, func, [])
def _approach_2([head | tail], func, result), do: _approach_2(tail, func, [func.(head) | result])
def _approach_2([], _func, result), do: result
end
To understand the difference between inefficient and efficient (tail) recursion
defmodule TestRecursion do
def inefficient([head | tail]), do: head + inefficient(tail)
def inefficient([]), do: 0
def efficient_tail_recursion(list), do: do_tail_recursion(list, 0)
def do_tail_recursion([head | tail], acc), do: do_tail_recursion(tail, acc + sum)
def do_tail_recursion([], acc), do: acc
end
Info:
- The
inefficient
function adds the stack trace of the calls recursively to compute the final output. - The efficient tail recursion doesn’t add stacktrace. Instead, it just calls the same function again. Internally, it just shifts the pointer to the beginning of the function. This doesn’t involve any overhead.
- To remember easily, consider the amount of memory one will need to have to execute the
inefficient
function while for theefficient
function, it just needs to call the function recursively. The function takes the responsibility of managing the state. - The efficient approach is called Tail Recursion. To know more about this, one can go here
Functions
Anonymous functions
How to define:
a = fn x -> x*x end # approach 1
b = & (&1 * &1) # approach 2
# call
a.(2)
b.(3)
Info:
- Whenever it comes to passing functions in functions, anonymous functions are used, since they can be passed around just like variables
- This is why collection library like
Enum.map
/Enum.reduce
take anonymous functions as second arguments. - Unlike python, in elixir, functions are not first class citizen. If we want to treat them as on we have to convert them to anonymous functions
Pattern matching can also be applied in function signatures in anonymous functions
func = fn
{:ok, result} -> IO.puts("Everything seems ok")
{:error, result} -> IO.puts("There is some error")
_ -> IO.puts("What! This has never happened before")
end
Converting normal function to an anonymous function
defmodule Test do
def square(x), do: x*x
end
# normal function call
Test.square(2)
# convert to anonymous function call, approach 1
a = fn x -> Test.square(x) end
# approach 2, convert to anonymous function call
a = & (Test.square(&1))
# approach 3, convert to anonymous function call
a = & (Test.square/1)
Magic of functions
Multiple ways of writing or shortening or pattern matching in a function
defmodule TestF do
def square(x) do
x * x
end
def sum(a, b), do: a + b
def check_cond(a, b) when a + b == 11, do: IO.puts("Sum is 11")
def check_cond(_a, _b), do: IO.puts("Something else")
def check_pat({:ok, _content} = result), do: IO.inspect(result, label: "input is ok")
def check_pat({:error, _content} = result), do: IO.inspect(result, label: "Input is error")
def check_pat(result), do: IO.inspect(result, label: "Something else entirely")
def check_list([2 | _tail] = result), do: IO.inspect(result, label: "Starts with 2")
def check_list([1, 2, 3 | _tail] = result), do: IO.inspect(result, label: "Starts with 1, 2, 3")
def check_list(result), do: IO.inspect(result, label: "Something else entirely")
end
Conditionals and Iterators:
case
. It is used to compare values against many patterns.
defmodule Test do
def case_check() do
case File.open("value.exs") do
{:ok, _fp} -> IO.puts("file exists and openend")
{:error, _other} -> IO.puts("file does not exist")
_ -> IO.puts("something strange has happened")
end
end
end
cond
. It is used to check different conditions
defmodule Test do
def condition_check(a, b) do
cond do
a + b == 5 -> IO.puts("sum is 5")
a * b == 121 -> IO.puts("both values are 11")
true -> IO.puts("Something else entirely")
end
end
end
if
statement
defmodule TestIf do
def approach_1(a, b) do
if a + b == 5 do
IO.puts("sum is 5")
else
IO.puts("sum is not 5")
end
end
def approach_2(a, b) do
if a + b == 5, do: IO.puts("sum: 5"), else: IO.puts("Not sum: 5")
end
end
for
loop
defmodule TestFor do
def square(list) do
for x <- list, do: x*x
end
end
Working with strings, charlist and binaries
Info:
- String are binaries internally
- Each character in the string is represented by at least 1 byte.
- To get each character in the string we can do:
String.graphemes(str)
String.charlist(str)
- Representation:
<<121>> # binary representation
<<121::8>> == <<121>> # each code point is represented by 8 bits
<<3::2, 4::4, 3::2>> == <<211>> # :: represent no of bits to be used
<<head, rest::binary>> = "banana
"
Use agent to store state
Create a cache store to keep values
defmodule CacheStore do
use Agent
def start_link() do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
def get(key) do
Agent.get(__MODULE__, fn map -> Map.get(map, key) end)
end
def put(key, value) do
Agent.update(__MODULE__, fn map -> Map.put(map, key, value) end)
end
def get_name(), do: __MODULE__
def get_all() do
Agent.get(__MODULE__, fn map -> map end)
end
end
Other Important Info
- Lists are internally represented as linked lists and they are not the same as array. So, going to the nth element of the linked list is not O(1) time