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.

  1. Define a module called Stack

     defmodule Stack do  # You are here
    
     end
    
  2. Add stack function to return an empty stack

     defmodule 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 the arguments and do: with a , and write the function inline.

  3. Add push function to push a value onto a stack

     defmodule 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, and stack is said to be the tail.

    • The new list, with the head, as val and tail as stack is returned as a new stack

  4. Add top function to get the value on top of a stack

     defmodule 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 empty

    • Notice how we use when condition before do 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 the push 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 of head 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 (_)

  5. Add pop function to delete the value on top of a stack

     defmodule 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 the Stack 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 and b, we can simply do a++b to concatenate them.

  • In Elixir we use IO.inspect() to print stuff to the console. Basically, it's console.log or print for Elixir. You can print any data structures, and can even use string interpolation. Let's say we have a variable x, then we can do something like IO.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 in start, goal and (optionally) a graph 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 provided

  • traverse_grid is a recursive helper function, that finally returns all possible paths in one single list

  • All 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!