Gopher exercise in Elixir: timeout implementation - stuck!

So, I’ve started to work on these exercises but using Elixir:
specifically, the first one:
Part 1 Create a program that will read in a quiz provided via a CSV file (more details below) and will then give the quiz to a user keeping track of how many questions they get right and how many they get incorrect. Regardless of whether the answer is correct or wrong the next question should be asked immediately afterwards.
Part 2 Adapt your program from part 1 to add a timer. The default time limit should be 30 seconds, but should also be customizable via a flag. Your quiz should stop as soon as the time limit has exceeded. That is, you shouldn’t wait for the user to answer one final questions but should ideally stop the quiz entirely even if you are currently waiting on an answer from the end user. Users should be asked to press enter (or some other key) before the timer starts, and then the questions should be printed out to the screen one at a time until the user provides an answer. Regardless of whether the answer is correct or wrong the next question should be asked. At the end of the quiz the program should still output the total number of questions correct and how many questions there were in total. Questions given invalid answers or unanswered are considered incorrect.

So my Part 1 implementation is this:

defmodule CsvQuiz do
  @moduledoc """
  Documentation for CsvQuiz.
  def csv_quiz(path) do
    [i, s] =
      |> Enum.reduce([0, 0], fn array, [iteration, acc] -> 
                                solution(array, iteration, acc) end)

    IO.puts("\nYou scored #{s} of #{i}.")


  defp parser(path) do!(path)
    |> String.split("\n")
    |> Enum.reject(fn x -> x == "" end)
    |>, ","))

  defp solution([question, answer], iteration, acc) do
    num = iteration + 1
    input = IO.gets("Problem ##{num}: #{question} = ") |> String.trim()

    result =
      if input == answer do
        acc + 1

    [num, result]

and that is how to run it and what I have as an output:

❯ iex -S mix 

Erlang/OTP 22 [erts-10.4.1] [source] [64-bit] 
[smp:12:12] [ds:12:12:10] [async-threads:1] [hipe]

Compiling 1 file (.ex)
Interactive Elixir (1.9.0) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> CsvQuiz.csv_quiz("/gophercises_in_elixir/lib/1_csv_quiz_problems.csv")
Problem #1: 5+5 = 10
Problem #2: 1+1 = 2
Problem #3: 8+3 = 11
Problem #4: 1+2 = 3
Problem #5: 8+6 = 1
Problem #6: 3+1 = 1
Problem #7: 1+4 = 1
Problem #8: 5+1 = 1
Problem #9: 2+3 = 1
Problem #10: 3+3 = 1
Problem #11: 2+4 = 1
Problem #12: 5+2 = 1

You scored 4 of 12.

But I am stuck with Part 2, I mean not about the code itself, but about what tools do I need to use?

Could you please critic my Part 1 and suggest where to move according to Part 2 description.

I guess I need to use Task.yield() but I am not sure to be honest.

Thanks a lot in advance!

There may be easier solutions, but this seems like a use case for GenServer

For example, the below is a “quiz” where each “question” is just an integer, and the correct answer is the same integer. You kick it off by calling Quiz.start_quiz with a path and optional timeout. That in turn will call GenServer.start_link/2, which will end up triggering the init/1 callback in the # Server part of the code. In that init callback you can initialize the state of your server by reading in the CSV file. From there it just kicks off a loop through all the questions. The server will terminate either after all questions have been answer or the timeout, which ever comes first (the scheduling of the timeout is handled in handle_call{:next, ..., ...)

Whenever the server terminates, we print out the results in the terminate/2 callback. But note from the docs

… it is not guaranteed that terminate/2 is called when a GenServer exits. For such reasons, we usually recommend important clean-up rules to happen in separated processes either by use of monitoring or by links themselves

I am sure there is plenty of room for improvement in this implementation. For instance, if you are in the middle of a question when the server times out, the results will be printed to the screen but the prompt for the next answer remains active. I’m not sure how to fix that at the moment.

defmodule Quiz do
  use GenServer

  # Client

  def start_quiz(path, timeout \\ 30_000) do
    {:ok, pid} = GenServer.start_link(__MODULE__, %{path: path, timeout: timeout})

    IO.gets("press enter to continue\n")


  defp loop(pid) do
    |> get_next_question()
    |> get_response()
    |> answer_current_question(pid)
    |> loop()

  defp get_response(question) do
    IO.gets("#{question} = \n")
    |> String.trim()

  defp get_next_question(pid) do, :next)

  defp answer_current_question(response, pid) do, {:answer, response})

  # Server

  @impl true
  def init(%{path: path, timeout: timeout}) do
    IO.inspect("starting quiz with path #{path}")
    questions = [1, 2, 3, 4, 5]
    total = length(questions)

     %{timeout: timeout, correct: 0, total: total, current_question: nil, questions: questions}}

  @impl true
  def terminate(:normal, %{correct: correct, total: total}) do
    IO.puts("Quiz over.  #{correct} questions answered correctly out of a total of #{total}")

  @impl true
  def handle_call(:next, _from, state) do
    # if current_question is nil, that means this is
    # the first question, so we schedule the timer now
    if state.current_question == nil do


  @impl true
  def handle_call({:answer, response}, _from, %{current_question: question} = state) do
    new_state =
      case is_correct(response, question) do
        true -> Map.update!(state, :correct, &(&1 + 1))
        false -> state

    {:reply, self(), new_state}

  @impl true
  def handle_info(:end_quiz, state) do
    {:stop, :normal, state}

  defp handle_next_question(%{questions: []} = state) do
    handle_info(:end_quiz, state)

  defp handle_next_question(%{questions: [next | remaining]} = state) do
    new_state =
      |> Map.put(:questions, remaining)
      |> Map.put(:current_question, next)

    {:reply, next, new_state}

  defp is_correct(response, question) do
    response == Integer.to_string(question)

  defp schedule_timer(state) do
    Process.send_after(self(), :end_quiz, state.timeout)

1 Like