Dwi Wahyudi
Senior Software Engineer (Ruby, Golang, Java)
This post will show my approach on handling fizzbuzz in Elixir. We will look at some features in Elixir programming language: Guard, pipe operator and first-class functions.
Overview
Previously, I’ve written fizzbuzz in Golang here,
Now let’s write fizzbuzz in Elixir. https://elixir-lang.org.
Fizzbuzz is a simple problem, but it is quite good at showing us how a programming language will do conditions (if-else) and iterations (loop).
In Elixir, there is no class (so there would be no instance variable), we use functions to do everything. Here, we will write a module (which we use to group functions).
defmodule Fizzbuzz do
end
Nothing really special, just an empty module.
Elixir Guard
We have seen Golang uses simple for loop or for-each loop and some classical if-else syntax. Java and Ruby are mostly the same. But in Elixir, there is no for-loop (we use recursion instead), and although there is if-else syntax, idiomatic Elixir uses Guard instead.
Now, what we gonna do? Just as I wrote above, we just need to use Guard.
- Guard is conditions placed on functions.
- Function will run if Guard conditions are satisfied.
Let say we have a function named calculate_discount, and it has params cost. Let say calculate discount here needs to run only if cost is more than 500. So the function will look like this:
def calculate_discount(cost) when cost > 500 do
# do something with the cost if cost is > 500
end
What if we have multiple conditions? In Java/Ruby/Golang we will create if-else, but in Elixir, with Guard, just create another function (with the same name) but with different Guard conditions.
def calculate_discount(cost) when cost > 500 do
# do something, if cost > 500
end
def calculate_discount(cost) when cost > 2000 do
# do something, if cost > 2000
end
def calculate_discount(cost) do
# the else condition, if none of above functions Guard satisfies
# in other words, do something if cost <= 500
end
With this in mind, we can solve our fizzbuzz in Elixir with 4 functions. Because if we look at Go Code there are 4 conditions.
- When fizzbuzz (divisible by 3 and 5).
- When fizz only (divisible by 3).
- When buzz only (divisible by 5).
- When neither fizz nor buzz, so return the number itself.
Our code in Elixir, will be like this, we will need rem function to calculate remainder of division.
defmodule Fizzbuzz do
def of(num) when rem(num, 3) == 0 and rem(num, 5) == 0, do: "FizzBuzz"
def of(num) when rem(num, 3) == 0, do: "Fizz"
def of(num) when rem(num, 5) == 0, do: "Buzz"
def of(num), do: num
end
Calling these functions is simple:
Fizzbuzz.of(3) # "Fizz"
Fizzbuzz.of(9) # "Fizz"
Fizzbuzz.of(5) # "Buzz"
Fizzbuzz.of(100) # "Buzz"
Fizzbuzz.of(15) # "FizzBuzz"
Fizzbuzz.of(30) # "FizzBuzz"
Fizzbuzz.of(2) # 2
Pipe Operator
Pipe operator |> is pretty simple to explain as it pipe the value into the first param of the function
a |> b()
# is the same as:
b(a)
c |> d(e)
# is the same as:
d(c, e)
order_data
|> calculate_discount()
|> calculate_tax()
|> calculate_delivery_price(delivery_price)
# is the same as:
calculate_delivery_price(calculate_tax(calculate_discount(order_data)), delivery_price)
First Class Functions
Now we have those 4 of functions in place, we can easily create a collection function that will receive num_start and num_end as range, so we can get fizzbuzz from 1 to 1000 for example.
Roughly speaking, this collection function will iterate from num_start to num_end and call Fizzbuzz.of(num) method multiple times, and collect/map these values into a list.
Elixir is a functional programming language, which means functions are first-class. https://en.wikipedia.org/wiki/First-class_function
We can pass of function as arguments to other functions.
def collection(num_start, num_end) do
(num_start..num_end)
|> Enum.map(&of(&1))
end
And we can call the function like this:
Fizzbuzz.collection(1, 20)
# [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz", 16, 17, "Fizz", 19, "Buzz"]
Our Fizzbuzz in Elixir is already completed. But what’s going on that collection function?
(num_start..num_end) is range data, which will return list of numbers from num_start to num_end. We pipe this list into Enum.map/2 function.
Enum.map is built-in Elixir function. https://hexdocs.pm/elixir/Enum.html#map/2.
It takes 2 params.
- The data that we operate our function on each of them.
- The function.
That collection function can be written verbosely like this:
def collection(num_start, num_end) do
(num_start..num_end)
|> Enum.map(fn(x) -> of(x) end)
end
Because of an Elixir feature, capture function, we can instead write call to Enum.map, like this:
|> Enum.map(&of(&1))
&1 is the param which is in the first position/order that we want to send to of function. We capture the of function, and send as argument each value of (num_start..num_end).
Reference:
https://elixir-lang.org/getting-started/modules-and-functions.html#function-capturing