The cond macro lets you list out a series of conditions, each with associated code. It executes the code corresponding to the first truthy conditions.
In the game of FizzBuzz, children count up from 1. If the number is a multiple of three, they say “Fizz.” For multiples of five, they say “Buzz.” For multiples of both, they say “FizzBuzz.” Otherwise, they say the number.
In Elixir, we could code this as follows:
1: | defmodule FizzBuzz do |
- | |
- | def upto(n) when n > 0, do: _upto(1, n, []) |
- | |
5: | defp _upto(_current, 0, result), do: Enum.reverse result |
- | |
- | defp _upto(current, left, result) do |
- | next_answer = |
- | cond do |
10: | rem(current, 3) == 0 and rem(current, 5) == 0 -> |
- | "FizzBuzz" |
- | rem(current, 3) == 0 -> |
- | "Fizz" |
- | rem(current, 5) == 0 -> |
15: | "Buzz" |
- | true -> |
- | current |
- | end |
- | _upto(current+1, left-1, [ next_answer | result ]) |
20: | end |
- | end |
The cond starts on line 8. We assign the value of the cond expression to next_answer. Inside the cond, we have four alternatives—the current number is a multiple of 3 and 5, just 3, just 5, or neither. Elixir examines each in turn and returns the value of the expression following the -> for the first true one. The _upto function then recurses to find the next value. Note the use of true -> to handle the case where none of the previous conditions match. This is the equivalent of the else or default stanza of a more traditional case statement.
There’s a minor problem, though. The result list we build always has the most recent value as its head. When we finish, we’ll end up with a list that has the answers in reverse order. That’s why in the anchor case (when left is zero), we reverse the result before returning it. This is a very common pattern. And don’t worry about performance—list reversal is highly optimized.
Let’s try the code in IEx:
| iex> c("fizzbuzz.ex") |
| [FizzBuzz] |
| iex> FizzBuzz.upto(20) |
| [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", |
| .. 13, 14, "FizzBuzz", 16, 17, "Fizz", 19, "Buzz"] |
In this case, we could do something different and remove the call to reverse. If we process the numbers in reverse order (so we start at n and end at 1), the resulting list will be in the correct order.
| defmodule FizzBuzz do |
| |
| def upto(n) when n > 0, do: _downto(n, []) |
| |
| defp _downto(0, result), do: result |
| defp _downto(current, result) do |
| next_answer = |
| cond do |
| rem(current, 3) == 0 and rem(current, 5) == 0 -> |
| "FizzBuzz" |
| rem(current, 3) == 0 -> |
| "Fizz" |
| rem(current, 5) == 0 -> |
| "Buzz" |
| true -> |
| current |
| end |
| _downto(current-1, [ next_answer | result ]) |
| end |
| end |
This code is quite a bit cleaner than the previous version. However, it is also slightly less idiomatic—readers will expect to traverse the numbers in a natural order and reverse the result.
There’s a third option. FizzBuzz transforms a number into a string. We like to code things as transformations, so let’s use Enum.map to transform the range of numbers from 1 to n to the corresponding FizzBuzz words.
| defmodule FizzBuzz do |
| def upto(n) when n > 0 do |
| 1..n |> Enum.map(&fizzbuzz/1) |
| end |
| |
| defp fizzbuzz(n) do |
| cond do |
| rem(n, 3) == 0 and rem(n, 5) == 0 -> |
| "FizzBuzz" |
| rem(n, 3) == 0 -> |
| "Fizz" |
| rem(n, 5) == 0 -> |
| "Buzz" |
| true -> |
| n |
| end |
| end |
| end |
This section is intended to show you how cond works, but you’ll often find that it’s better not to use it, and instead to take advantage of pattern matching in function calls. The choice is yours.
| defmodule FizzBuzz do |
| def upto(n) when n > 0, do: 1..n |> Enum.map(&fizzbuzz/1) |
| |
| defp fizzbuzz(n), do: _fizzword(n, rem(n, 3), rem(n, 5)) |
| |
| defp _fizzword(_n, 0, 0), do: "FizzBuzz" |
| defp _fizzword(_n, 0, _), do: "Fizz" |
| defp _fizzword(_n, _, 0), do: "Buzz" |
| defp _fizzword( n, _, _), do: n |
| end |