# Refactoring a Function in Elixir

Engineer

Zach Liss

Over the past year, I have been using my DockYard Days (time dedicated to professional development and passion projects) and some free time to pick up Elixir. I’ve read a few textbooks, worked through this year’s Advent of Code programming puzzles, and participated in DockYard’s Mentor/Mentee Program. Throughout this learning process, I’ve also been working towards building a small quiz game to practice implementing various concepts as I come across them.

Recently, I’ve wrapped up Part 1 of Designing Elixir Systems with OTP and felt this was a great opportunity to revisit and refactor some of the old code I’ve written with core programming principles demonstrated in the book.

## Core programming principles

• Build functions at a single level of abstraction
• Make decisions in function heads where possible
• Name concepts with functions
• Shape functions for composition
• Build single-purpose functions

Let’s step through applying these core principles to an `answer_question` function from the quiz game application. `answer_question` lives inside of a `Game` module and handles the logic around what should happen when a user with a `user_name` attempts to answer a `question` with a `guess`.

``````# game.ex

def answer_question(%Game{current_question: current_question} = game, guess, user_name) do
case(current_question.mascot == guess) do
true ->
game =
game
|> increase_score_for_user(user_name)
|> check_for_winning_user(user_name)
|> Map.put(:current_question, get_random_question())

{:ok, game}

_ ->
{:error, :incorrect}
end
end
``````

## Functions should be at single layer of abstraction

Our `answer_question` function currently has three different layers of abstraction.

• `case(current_question.mascot == guess) do` - We’re making a decision at the `guess` level to determine if the response from a user is correct
• `game = game |> increase_score_for_user(user_name) |> check_for_winning_user(user_name)` - Game level operations occur as we a advance our `game` token.
• `|> Map.put(:current_question, get_random_question())` - Elixir datatypes level abstraction

The first thing we can do is rework the pipeline. The `|> Map.put(:current_question, get_random_question())` can be pulled into a single purpose and composable function.

``````# game.ex

def answer_question(%Game{current_question: current_question} = game, guess, user_name) do
case(current_question.mascot == guess) do
true ->
game =
game
|> increase_score_for_user(user_name)
|> check_for_winning_user(user_name)
|> select_question() # add select_question() to the pipeline

{:ok, game}

_ ->
{:error, :incorrect}
end
end

defp select_question(game) do
Map.put(game, :current_question, get_random_question())
end
``````

With the above change, we’ve also touched on three more core principles:

• Name concepts with functions
• Shape functions for composition
• Build single-purpose functions

`select_question` was created to name the `Game` level concept of selecting a question. The only purpose of the function is to wrap an Elixir datatype operation. We shaped it for composition in our pipeline by accepting the `%Game{}` as the first argument and also returning it.

Next we can create a new `Response` module to abstract away determining the “correctness” of a `guess`. In `answer_question`, we create a new response and use `response.correct` in our case statement.

``````# response.ex

defmodule Response do
defstruct ~w[guess game user_name correct]a

def new(game, guess, user_name) do
%__MODULE__{
guess: guess,
user_name: user_name,
game: game,
correct: correct?(game, guess)
}
end

defp correct?(%Game{current_question: current_question} = game, guess) do
current_question.mascot == guess
end
end
``````
``````# game.ex

def answer_question(%Game{current_question: current_question} = game, guess, user_name) do
response = Response.new(game, guess, user_name)
case(response.correct) do
true ->
game =
game
|> increase_score_for_user(user_name)
|> check_for_winning_user(user_name)
|> select_question() # add select_question() to the pipeline

{:ok, game}

_ ->
{:error, :incorrect}
end
end
``````

This is a good start. The overall intention of this function is easier to understand now that we’ve updated it to only use one level of abstraction. We can do better and continue to break it up further.

## Make decisions in function heads where possible

Now that `response.correct` is stored in a struct, we can remove the `case` statement in favor of some additional clauses for the `answer_question` function.

``````# game.ex

def answer_question(%Game{current_question: current_question} = game, guess, user_name)
when is_binary(guess) do
response = Response.new(game, guess, user_name)
end

def answer_question(game, %Response{correct: true} = response, user_name) do
game =
game
|> increase_score_for_user(user_name)
|> check_for_winning_user(user_name)
|> select_question()

{:ok, game}
end

def answer_question(_game, %Response{correct: false} = response, _user_name) do
{:error, :incorrect}
end
``````

By adding several new `answer_question` clauses which pattern match on `%Response{correct: true}` and `%Response{correct: false}` we’ve pulled the decision from the previous `case` statement and reduced each function to a single purpose.

## Wrap Up

So far `Designing Elixir Systems with OTP` has provided some handy tools. I feel that these programming principles will continue to help me write code that has clearer intentions, is testable, and remains maintainable over time. I’m looking forward to seeing what’s next in Part 2!

DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js, React.js, Ruby, and Elixir. With a nationwide staff, we’ve got consultants in key markets across the United States, including San Francisco, Los Angeles, Denver, Chicago, Austin, New York, and Boston.