ruby vs elixir (2 Part Series)
I've been working through a lot of common interview questions recently as part of my daily reps, something I do everyday to force me to learn constantly and hone my craft. I decided rather than solve each problem in just my native language of Ruby, I'd solve each problem twice, once in Ruby and once in Elixir. You can read a little bit more about why I chose to do that here.
I figured while I'm doing that, it might be interesting to write about some of the key differences I notice between the two. So that's what this is, a quick breakdown of some key differences, not a step-by-step guide of how I came to the solutions.
In these examples, I tried my best to use each language in the truest form possible, meaning in Ruby I followed a strict object oriented approach and relied primarily on objects, mutation, instance variables, and iteration, and in Elixir I followed a strict functional approach and relied mainly on functions, piping, pattern matching, and recursion.
I also tried my best to make the two solutions read as similarly as possible by using similar function names, variable names, and general order of operations.
# ruby class FizzBuzz def initialize(max) @max = max @fizz_buzz_array =  end def play populate_fizz_buzz_array display_fizz_buzz end private def populate_fizz_buzz_array 1.upto(@max).each do |num| value = get_fizz_buzz_value(num) add_value_to_fizz_buzz_array(value) end end def get_fizz_buzz_value(num) if num % 15 == 0 "FizzBuzz" elsif num % 3 == 0 "Fizz" elsif num % 5 == 0 "Buzz" else num.to_s end end def add_value_to_fizz_buzz_array(value) @fizz_buzz_array << value end def display_fizz_buzz puts @fizz_buzz_array end end ## USAGE fizzbuzz = FizzBuzz.new(15) fizzbuzz.play # 1 # 2 # Fizz # ...
# elixir defmodule FizzBuzz do def play(max) do max |> create_fizz_buzz_list() |> display_fizz_buzz() end defp create_fizz_buzz_list(list, 0), do: list defp create_fizz_buzz_list(list \\ , num) do num |> get_fizz_buzz_value() |> add_value_to_list(list) |> create_fizz_buzz_list(num - 1) end defp get_fizz_buzz_value(num) do cond do rem(num, 15) == 0 -> "FizzBuzz" rem(num, 3) == 0 -> "Fizz" rem(num, 5) == 0 -> "Buzz" true -> to_string(num) end end defp add_value_to_list(value, list) do [value | list] end defp display_fizz_buzz(), do: :ok defp display_fizz_buzz([head | tail]) do IO.puts(head) display_fizz_buzz(tail) end end ## USAGE FizzBuzz.play(15) # 1 # 2 # Fizz # ...
Take look at 'create_fizz_buzz_list' and 'populate_fizz_buzz_array'
# ruby def populate_fizz_buzz_array 1.upto(@max).each do |num| value = get_fizz_buzz_value(num) add_value_to_fizz_buzz_array(value) end end
# elixir defp create_fizz_buzz_list(list \\ , num) do num |> get_fizz_buzz_value() |> add_value_to_list(list) |> create_fizz_buzz_list(num - 1) end
In the Ruby version, unless your an expert at the language, you almost need to take a minute to examine the code just to make sense of what's going on, whereas the Elixir counterpart almost reads like a book (all though the final line might be a bit confusing.)
Of course, I could have written Ruby to be a bit more declarative, but the point of this was to follow common patterns and write code the way I actually would in the real world, and I think Elixir does a better job at encouraging the use of a more declarative style of coding.
Look at the function/method where I add the fizz buzz value to the collection:
# ruby def add_value_to_fizz_buzz_array(value) @fizz_buzz_array << value end
# elixir defp add_value_to_list(value, list) do [value | list] end
Notice how in Ruby, I called it "add_value_to_fizz_buzz_array," but in Elixir, I dropped the "fizz_buzz" and just called it "add_value_to_list." This was very intentional. In Ruby, the method call is adding the value only to the predefined fizz_buzz_array, however, the Elixir function is a bit more generic and adds the element to whatever list the function is called with.
This means that the Elixir function is quite a bit more flexible than the Ruby version, and could easily be abstracted into some other module and reused throughout the code.
Again, I know I could have written Ruby with a more functional approach and achieved the same behavior, but the fact is, in Ruby, the standard way to make changes is to do exactly what I did and manipulate instance state internally through the use of methods.
Because lists in Elixir are actually linked lists and not indexed arrays like their counterpart in Ruby, I had to take a different approach when interacting with them.
In Ruby it's cheap and easy to append to the end of an array so that's become the defacto way to add elements to an array, which is exactly how I did it in this example.
On the other hand, in Elixir it's easier and requires much less processing power to add elements to the beginning of a list, due mainly to the way linked lists work.
That alone isn't very noteworthy, but what I found interesting about it was it changed the entire direction I worked through the numbers in. In Ruby, because I was adding to the end of the array, it made the most sense to start at 1 and iterate up to 15:
# ruby 1.upto(@max).each do |num| # ... end
However, in Elixir, because I was adding to the beginning of the list, it made more sense to start at 15 and recursively work my way down to 1.
# elixir create_fizz_buzz_list(num - 1)
In a functional language like Elixir, if something works once, it works twice. There's almost always a one-to-one relationship between input and output. With object oriented languages on the other hand, you have to be a little bit more careful.
Notice how I'm never clearing the @fizz_buzz_array variable in Ruby, just adding new elements to it. If I try to 'play' a specific instance of FizzBuzz one time, it works like a charm. But what happens if I try to play the same instance twice?
# ruby fizzbuzz = FizzBuzz.new(3) fizzbuzz.play # 1 # 2 # Fizz fizzbuzz.play # 1 # 2 # Fizz # 1 # 2 # Fizz
This is a glitch you probably wouldn't think to test, and something you might not even realize is happening unless you have a deep understanding of Ruby and the Object Oriented programming pattern. This glitch is easily solvable. I could clear the array in every time I populate it, or I could move the entire "populate_fizz_buzz_array" method call from the play function to the initialize function so the array only gets populated once, but I decided to leave it in as an example of how in OOP it's often times very easy to have less-than-ideal side effects slip under the radar.
When I came up with the “things I noticed” list, it was after I had already written all of the code. I tried to write honest stream of consciousness of what I noticed while I looked back over the code. I know a lot of these points seem like I'm really hyping up Elixir and shitting on Ruby, but that wasn't my intention going in, that was just the honest list of the things that stood out to me when I compared the code.
If it seems like I have a strong bias towards Elixir, I think that's because Elixir is the new, exciting language that I've only recently started working with, whereas Ruby is the old, almost boring language that I've worked with for years. Elixir to is the 'greener grass on the other side,' so right now I know I'm prone to seeing only the good and not the bad. I'm sure once I become a more familiar with Elixir I'll have a more balanced view of the good and the bad of both, but for now, I definitely find myself drawn to the way things are done in Elixir.
Does anything else stand out to you about the difference between the two solutions? Which version do you prefer?
If you found this article interesting or informative and want to see more articles like it, let me know! And feel free to let me know what you think would be a good topic for my next Ruby vs Elixir article! (Fibonacci? Factorials? Something Else?)