Playing with the syntax - Functional programming with Elixir
Disclaimer: I am learning the language myself, and in no way i am even close to being an expert, so consider this blog, as a journey diary, instead of a guide.
What's this blog about?
Getting used to short-hand syntax, and the pattern of writing code, along with the manipulation of various data-structure.
Getting Familiar
Working with lists and enums
Remember, data is immutable, so unlike in oops, we can't make a class, and then make a stack object which is manipulated in place.
Things will be much simpler here, let's go through the implementation.
Okay, so the question. I am gonna assume that you know what a stack is.
Define a module called
Stack
defmodule Stack do # You are here end
Add
stack
function to return an empty stackdefmodule Stack do def stack(), do: [] # You are here end
Here, we have used an inline syntax to write a function, where instead of using
do-end
block, we separate thearguments
anddo:
with a,
and write the function inline.Add
push
function to push a value onto a stackdefmodule Stack do def stack(), do: [] def push(stack, val) do # You are here [val|stack] end end
In the
push
function, we receive the current stack and the value to be pushed as arguments.[val | stack]
is a way of appending a value at the beginning of a list, here,val
is said to be the head of the list, andstack
is said to be the tail.The new list, with the head, as
val
and tail asstack
is returned as a new stack
Add
top
function to get the value on top of a stackdefmodule Stack do def stack(), do: [] def push(stack, val) do [val|stack] end def top(stack) when stack==[], do: NULL # You are here def top([head | _stack]) do head end end
For the
top
function, we define two cases, one when the stack is empty, and one when it's not emptyNotice how we use
when
condition beforedo
to specify when this function should be called.For an empty stack, we return
NULL
When the stack is not empty, the default
top
the function is executed, and to extract the top element, we use the concept of pattern matching. What we are doing here is exactly similar to what we did in thepush
method.Pattern matching can be done in the arguments themselves. Elixir will pattern-match the receiving arguments and assign the values accordingly.
We are representing
stack
as a combination ofhead
and_stack
(tail), here,_
at the start represents that we are not using this variable. It's a convention to name the unused variables starting with an underscore (_
)
Add
pop
function to delete the value on top of a stackdefmodule Stack do def stack(), do: [] def push(stack, val) do [val | stack] end def top(stack) when stack==[], do: NULL def top([head | _stack]) do head end def pop([]), do: [] # You are here def pop([_head | stack]) do stack end end
For
pop
function as well, we handle two cases.When the stack is empty, return an empty stack, and here, instead of using
when
, we use pattern matching to match the appropriate function call.When the stack is not empty, we use the exact same pattern matching as used in
top
function, but, instead of using the head, we use the tail part of the matched list, and return the "tail" without the head, effectively popping the first (top) element.
Pattern matching is a really powerful tool, and you will see it being used extensively in the Elixir world!
Testing what we just built
For now, we gonna manually test our code. Let's write some function calls-
stack |> push(1) |> push(1) |> push(2)
|> pop |> push(3) |> push(5) |> pop |> top
Before running the code above, try guessing the output...
Well, if you think the answer will three, then you got it right. Here, we see a good example of the flow of using a pipe operator through a series of functions.
This stack example should make you familiar with most of the syntax used in and around Elixir.
Some random gotchas
And some spaghetti code to find the shortest path
These are the key points of elixir that make it different from your everyday programming language syntax.
You will see the use of
|
operator a lot in Elixir, the gist of the pipe operator is the same as we saw in theStack
implementation example. The|
is also used with maps, structs, and other data structures holding a collection of data.One more magical syntax is for concatenating lists. Let's say we have two lists,
a
andb
, we can simply doa++b
to concatenate them.In Elixir we use
IO.inspect()
to print stuff to the console. Basically, it'sconsole.log
orprint
for Elixir. You can print any data structures, and can even use string interpolation. Let's say we have a variablex
, then we can do something likeIO.inspect("value of x is #{x}")
With this stuff in mind, let's try to look at some not-so-easy code~
Backstory
So, I had to write a shortest path finding algorithm using BFS in a graph, for a robot that I was building. Writing BFS is a trivial task, but in Elixir, I had to bang my head on the wall to get things working, and I can assure you that there will be a better way of writing this code!
The spaghetti
defmodule Pathfinder do
@grid %{
1 => [2, 4],
2 => [1, 3, 5],
3 => [2],
4 => [1, 7],
5 => [2, 6, 8],
6 => [5, 9],
7 => [4, 8],
8 => [5, 7],
9 => [6]
}
defp traverse_grid(grid, start, goal, traversed_path) do
if start == goal do
traversed_path
else
grid[start]
|> Enum.map(fn cell ->
if cell in traversed_path do
nil
else
traverse_grid(grid, cell, goal, [cell | traversed_path])
end
end)
|> Enum.reject(fn x -> x == nil end)
|> List.flatten()
|> Enum.reverse()
end
|> Enum.reverse()
end
@doc """
#Function name:
shortest_path
#Details:
To find the shortest path from start to goal location
#Example call:
shortest_path(2, 4)
"""
def shortest_path(start \\ 1, goal \\ 8, grid \\ @grid) do
all_possible_paths = traverse_grid(grid, start, goal, [start])
# extract the shortest path from all possible paths
if(Enum.count(all_possible_paths) == 1) do
all_possible_paths
else
[_ | path_without_start] = all_possible_paths
goal_idx = Enum.find_index(path_without_start, fn x -> x == goal end)
{p1, p2} = Enum.split(all_possible_paths, goal_idx + 2)
if Enum.count(p1) < Enum.count(p2), do: p1, else: p2
end
end
end
I won't be explaining my thought process in writing this piece of code, as it would trigger my PTSD, and would take up a whole other blog. But if you want to play around with it, here are the things you need to know:
shortest_path
is the function that takes instart
,goal
and (optionally) agraph
representing connected paths.@grid
is a graph in the form of an adjacency list, and is used as a default graph when no graph is providedtraverse_grid
is a recursive helper function, that finally returns all possible paths in one single listAll paths are then split into different lists containing each path, and the shortest one is returned.
An example would look like~
iex(10)> shortest_path(1, 9) [1, 2, 5, 6, 9] iex(11)>
Here is an ugly drawing to visually represent the graph and the path:
With all that said, this code might still be buggy, so ping if you find some edge cases or something like that. And do try to mess around with the code a little, put some inspect
statements to see how things are working!
That's enough to take in!
Okay, phew! We have got some taste of the Elixir language. Now we can go crazy and solve LeetCode in Elixir.
But you might have noticed a problem with Elixir.
How do we maintain a state of a variable in a completely immutable language?! And how can we access that state-managed variable in different parts of our code? We can't declare global variables, we can't change existing variables, then what's the solution?
Elixir solves this by using GenServers. About them, in the next blog. Sayonara!
Subscribe to my newsletter
Read articles from Husain Shahid Rao directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Husain Shahid Rao
Husain Shahid Rao
A full stack developer, and a guy who codes up useless stuff cuz it's fun!