Introduction

This is a problem from Codewars.

Problem

The first input array contains the correct answers to an exam, like [“a”, “a”, “b”, “d”]. The second one is “answers” array and contains student’s answers.

The two arrays are not empty and are the same length. Return the score for this array of answers, giving +4 for each correct answer, -1 for each incorrect answer, and +0 for each blank answer(empty string).

If the score < 0, return 0.

For example:

checkExam(["a", "a", "b", "b"], ["a", "c", "b", "d"]) → 6
checkExam(["a", "a", "c", "b"], ["a", "a", "b",  ""]) → 7
checkExam(["a", "a", "b", "c"], ["a", "a", "b", "c"]) → 16
checkExam(["b", "c", "b", "a"], ["",  "a", "a", "c"]) → 0

Manual Solution (Human Intelligence)

The problem mentions two arrays. Since we are using Elixir, we will refer to them as lists.

Steps:

  1. Start with a score of 0
  2. Look at the first entry in the correct_answer list
  3. Look at the entry in the answer list at the same position as correct_answer list
    • If the answer entry is empty, add 0 to score
    • Otherwise compare the entry in the correct_answer list with the entry in the answer list
      • If both entries match, then add 4 to the score
      • Otherwise, subtract 1 from the score
  4. Look at the next entry in the correct_answer list and repeat Step 3 if the entry exists
  5. If there are no more entries in the correct_answer list
    • If the score is negative, then score is 0
    • Otherwise, report score and stop

Solution (Elixir)

We could walk through the correct_answer list and the answer list in parallel and compare them as outlined in the manual solution. But, there is a better way.

We can combine both lists so that we can walk through that combined list and compare entries in the list to score.

The structure of our code will be:

Combine list
|> Check answers and determine the score of the exam
|> Report final score

Combine lists

The Elixir function Enum.zip/2 allows us to zip corresponding elements from two enumerables (in our case lists) into one list of tuples.

Enum.zip(correct_answer_list, answer_list)

In our example, we are combining two enumerables with the Enum.zip/2 function, but the Elixir function Enum.zip/1 allows us to combine more than two enumerables, as well.

Check answers and determine the score of the exam

Next, we want to check the entries in the list. Each entry is a tuple in the form of {correct_answer, answer}. We can compare elements within a tuple with each other. There are several ways to do so in Elixir. Let’s take a look at a few of those approaches.

Build our own function to walk through list

defp compare([], acc), do: acc

defp compare([head | tail], acc) do
  compare(tail, score(head, acc))
end

In the above code, we have created two compare/2 functions to walk through the combined list. They both are private functions which we will call from our main function.

These functions accept two parameters. The first one is the combined list we built earlier. The second parameter is an accumulator, acc, with which we accumulate the results of our comparisions.

These functions allow us to walk through the list recursively and, at the same time, accumulate the result of each of the comparisions.

The first function is a ‘stop’ condition for our recurssion. When we reach the end of the list, we stop and return the score. Note that, as the second function calls itself as the last thing, it is a tail call and Elixir/Erlang will accordingly perform tail-call optimization.

defp score({_expected, answer}, acc) when answer == "", do: acc
defp score({expected, answer}, acc) when expected == answer, do: acc + 4
defp score({expected, answer}, acc) when expected != answer, do: acc - 1

Next we created three private score/2 functions which are called from compare/2. They allow us to compare the expected answer with the actual answer. They allow us branching of conditional logic using multiclause functions.

These functions accept two parameters. The first one is a tuple in the form of {expected_answer, answer} which is an element of the combined list we built earlier. The second parameter is an accumulator, acc, with which we accumulate the results of our comparisions.

We could have used conditional constructs such as if, case, and cond etc. in the compare/2 function. However, by using multiclause functions, the logic for handling the conditions is seperated from the higher level (calling) function. The code for conditions is cleaner and self-descriptive.

These three functions will update the acc (score) and return it to the compare/2 function.

Use Enum module to reduce the list to score

Enum.zip(correct, answer)
|> Enum.reduce(0, &score/2)

With this approach, we use the Elixir Enum.reduce/3 module to walk through the combined list. The function takes in three arguments:

  1. enumerable (combined list produced by Enum.zip/2)
  2. accumulator (to store the result of reduction)
  3. function (to compare the answers)

The accumulator will start with 0 (as per our manual Step 1). We will use the score/2 functions (as discussed in the earlier approach) to calculate the score. The score/2 function will get two parameters - a tuple for comparision and the accumulator.

Report final score

defp final_score(score) when score < 0, do: 0
defp final_score(score), do: score

Lastly, we can compare the score to check the final tally. If the score is negative, we return the tally as 0. Otherwise, we return the score as a tally.

Final code

defmodule CheckExam do
  def check_exam(correct, answer) do
    Enum.zip(correct, answer)
    |> Enum.reduce(0, &score/2)
    |> final_score()
  end

  defp score({_expected, answer}, acc) when answer == "", do: acc
  defp score({expected, answer}, acc) when expected == answer, do: acc + 4
  defp score({expected, answer}, acc) when expected != answer, do: acc - 1

  defp final_score(score) when score < 0, do: 0
  defp final_score(score), do: score
end