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 languaage that I learnt. I would like to list all my learnings over here so that this can be the sole article I can refer to if I want to quickly read up 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 few variances of recursion over here. This will primarily help in understanding:
- How to convert and iterative solution to a recursive one.
- How to write an efficient recursive function
To understand how to convert for
loop into the recusive function
Problem - Iterate over first 100 numbers and calculate it’s 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 rest of the arguments in the function.
Approach 2:
Problem - Given a list of strings, return a list of it’s 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 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 stacktrace of the calls recursively to compute the final output. - The efficient tail recursion doesn’t add stacktrace. Instead it just calls 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
inefficient
function while for theefficient
function it just needs to call the function recursively. The function takes the responsibility of managing 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
Originally posted at - https://blog.aashd.me/2021/10/16/learning-elixir.html
Top comments (0)