DEV Community

Steven Solomon
Steven Solomon

Posted on • Updated on

My First Week With Elixir As A Rubyist

I recently started learning Elixir for an upcoming project. As part of my usual prep work, I have studied the language documentation, walked through the Koans, and practiced some katas on Exercism.io.

The end result is a few changes to how I approach problem solving, that I want to share with you.

Before we jump in, some things you might find confusing are:

1) Function signatures include the number of parameters that they take, referred to as arity. A function signature any?(collection, fn) would be referred to as any?/2.

2) Unused parameters are named but are prefixed with an underscore.

3) iex is the name of the REPL in Elixir.

Pattern matching and Decomposition

Pattern matching as a first class idea is new to me. Usually I would avoid case statements and destructuring, and favor polymorphism, but I have begun to play with some of the interesting cases that are possible.

Let's look at an example. Here I match the variable a to a pattern of a string literal.

iex> a = "Some String" # Binds string to a
"Some String"
iex> "Some String" = a # Matches a to the pattern
"Some String"
Enter fullscreen mode Exit fullscreen mode

Pattern matching throws an error when there is not a match.

iex> "Not a Match" = a # Throws an error
** (MatchError) no match of right hand side value: "Some String"
Enter fullscreen mode Exit fullscreen mode

It is possible to destructure lists using pattern matching. Here you can see how to separate the first element of a list from the rest of it.

iex> [head | tail] = [1, 2, 3, 4, 5]
iex> head
1
iex> tail
[2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

You can also destructure more complex data types. For instance, grabbing a key out of a map:

iex> user = %{id: 1, login: %{user_name: 'bob114', password: 'Apple12Sauce!'}}
iex> %{id: id} = user
iex> id
1
iex> %{login: %{user_name: user_name}} = user
iex> user_name
bob114
Enter fullscreen mode Exit fullscreen mode

Recursion First

I had to adapt my style to embrace recursion. That last time that I wrote so much recursive code was in a computer science course. This is a drastic style change but would be best demonstrated through an example of navigating collections.

Let's create a function to determine if any element is contained in a collection, and name the function any?/2.

I start by asking, "What is the base case for any?/2?".

It would be when the list parameter is empty. Using pattern matching we can define a function header that will match the scenario where the first element passed is an empty list, and return false.

defmodule Example do
  def any?([], _a), do: false # Matches when the first param is an empty list
end
Enter fullscreen mode Exit fullscreen mode

Next I think ask, "What could be the recursive case?"

When the list is not empty, I try to divide the problem into smaller parts, allowing me to hit the base cases. There are two possibilities, 1) the first element is equal to the variable, 2) it isn't and we need to keep looking. If we implement scenario one, then we have the head equal to a, so we return true.

defmodule Example do
  def any?([], _a), do: false
  def any?([head | _tail], a) do
    if head == a do
      true
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In the last scenario we deal with recursing the rest of the list.

defmodule Example do
  def any?([], _a), do: false
  def any?([head | tail], a) do
    if head == a do
      true
    else 
      any?(tail, a) 
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Maintaining state

In Elixir state is managed inside of processes. Each process emulates state transitions by using recursion. Processes allow their state to be changed or queried by other processes via messages. Rather than show you the low level tooling (spawn, Agent, or Task), the most high-level construct I am aware of is the GenServer.

GenServer is a core language construct that wraps all the complexity around a process sending and receiving messages. It is a module that has two types of functions, asynchronous and synchronous.

Synchronous functions are calls, they will reply to the calling process with some data. They do this by returning a tuple like {:reply, data, state}. The :reply tells the server to respond to the caller, the data is what to send in the reply, and the state is the new state for the process.

defmodule OurServer do
  use GenServer

  ...

  def call({:message, caller_data}, _from, state) do
    {:reply, caller_data, [caller_data]}
  end
end
Enter fullscreen mode Exit fullscreen mode

Asynchronous functions are casts, they do not reply to callers. They return a tuple like {:noreply, state}. :noreply tells the server not to reply, and state is the new state for the process.

defmodule OurServer do
  use GenServer

  def cast({:message, caller_data}, state) do
    {:noreply, caller_data}
  end
end
Enter fullscreen mode Exit fullscreen mode

In future articles, I plan to discuss GenServers more deeply, but that's all for now.

Conclusion

I am venturing out into the wilderness of Functional Languages by trying Elixir for the first time.

The things I found the most interesting on week one are:

  • Pattern matching
  • Recursion first
  • Emulating state with processes

If you enjoyed this article and want to read more about Elixir, please leave a comment or a like below. Thanks!

Top comments (4)

Collapse
 
edisonywh profile image
Edison Yap

Great writeup! I've also started learning Elixir not long ago and have been loving every minute of it.

Just a slight suggestion, you can add guards into your function headers too, so your recursion example could even be refactored to something like: (btw, you missed out a do in your defmodule)

defmodule Example do
  def any?([], _a), do: false
  def any?([head | _tail], a) when head == a, do: true
  def any?([_head | tail], a), do: any?(tail, a)
end

This uses what we call guard clause, which is the when head == a part of the code.

Basically, most of the time if you see an if statement concerning the arguments passed into the function, you probably could do away with if/else and pattern match on the function head to do the same thing :)

Collapse
 
fidr profile image
Robin

You can also match on equality by re-using the same param name in the function definition:

defmodule Example do
  def any?([], _a), do: false
  def any?([a | _tail], a), do: true
  def any?([_head | tail], a), do: any?(tail, a)
end
Collapse
 
ssolo112 profile image
Steven Solomon

I really dig the pattern for head being equal to the element you are looking for.

Collapse
 
ssolo112 profile image
Steven Solomon • Edited

Thank you for the tip about guard clauses, I definitely love getting rid of conditions =)