Grade School - Exercism Elixir Solution & Tutorial


Percy Grunwald's Profile Picture

Written by Percy Grunwald

— Last Updated February 22, 2019

Today’s Exercism Elixir coding challenge required us to implement a module to add students to a “database” (an Elixir map) and get them out again. This challenge was a nice little introduction to some commonly used map-related functions such as Map.get/3 and Map.update/4, which are both used a lot and really worth knowing.

Solution video with explanations

This is my daily live stream video solution to this problem, in which I guide you through each step of the way and explain my logic in approaching the problem and selecting the best Elixir functions for the job.

Explanation of the solution

Adding students to the database by name and grade

The first function to implement was add/3, which adds a student to a particular grade in our school “database”. Here’s a flow of how it should work:

Initial DB: %{}
Add Aimee to Grade 2: %{2 => ["Aimee"]}
Add Percy to Grade 2: %{2 => ["Percy", "Aimee"]} (order does not matter)
Add Ben to Grade 3: %{2 => ["Percy", "Aimee"], 3 => ["Ben"]}
Add Greg to Grade 3: %{2 => ["Percy", "Aimee"], 3 => ["Greg", "Ben"]}

Given this behavior, I thought the most efficient way to implement that functionality was with Map.update/4. This is a super useful function that replaces this workflow with a single line:

current_list = Map.get(db, grade, [])
updated_list = [name | current_list]
updated_db = Map.put(db, grade, updated_list)

This same workflow is a one-liner using Map.update/4:

@doc """
Add a student to a particular grade in school.
"""
@spec add(map, String.t(), integer) :: map
def add(db, name, grade) do
  db
  |> Map.update(grade, [name], fn current_list -> [name | current_list] end)
end

You might be wondering why the init value in the code above is [name] and not []. The reason is that the anonymous function doesn’t get called if the key isn’t in the map already, it will just put the initial value at the specified key. This trips a lot of people up, so be careful with this.

Getting all students in a particular grade

This was a super simple function to implement: it basically just boils down to doing a Map.get/3 on the “database” for an input grade and returning an empty list [] if the grade doesn’t exist in the DB:

@doc """
Return the names of the students in a particular grade.
"""
@spec grade(map, integer) :: [String.t()]
def grade(db, grade) do
  db
  |> Map.get(grade, [])
end

In the solution video, I explain that the same functionality can be achieved using the “bracket notation” for accessing values in a map (called the Access behaviour) and the || operator:

db[grade] || []

I recommend using Map.get/3 in all cases, since it gives you much more control and it’s pipe-able.

Getting students in all grades, sorted by grade and name

The final function we needed to implement was to output a list of all grades and students in each grade.

For example, given a map like this:

%{
  1 => ["Percy", "Greg"],
  3 => ["Kyle"],
  4 => ["Jennifer", "Christopher", "Bart"],
  6 => ["Kareem"]
}

Our sort/1 function should return:

[
  {1, ["Greg", "Percy"]},
  {3, ["Kyle"]},
  {4, ["Bart", "Christopher", "Jennifer"]},
  {6, ["Kareem"]}
]

Notice in the output above that the list is sorted first by the grade and then the name (i.e. the list of names is in alphabetical order).

This was an easy enough solution using Map.to_list/1, Enum.map/2 and Enum.sort/1:

@doc """
Sorts the school by grade and name.
"""
@spec sort(map) :: [{integer, [String.t()]}]
def sort(db) do
  db
  |> Map.to_list()
  |> Enum.map(fn {grade, list_of_students} ->
    {grade, Enum.sort(list_of_students)}
  end)
end

Full solution in text form

Here’s the full solution in text form, in case you want to look over the final product:

defmodule School do
  @moduledoc """
  Simulate students in a school.

  Each student is in a grade.
  """

  @doc """
  Add a student to a particular grade in school.
  """
  @spec add(map, String.t(), integer) :: map
  def add(db, name, grade) do
    db
    |> Map.update(grade, [name], fn current_list -> [name | current_list] end)
  end

  @doc """
  Return the names of the students in a particular grade.
  """
  @spec grade(map, integer) :: [String.t()]
  def grade(db, grade) do
    db
    |> Map.get(grade, [])
  end

  @doc """
  Sorts the school by grade and name.
  """
  @spec sort(map) :: [{integer, [String.t()]}]
  def sort(db) do
    db
    |> Map.to_list()
    |> Enum.map(fn {grade, list_of_students} ->
      {grade, Enum.sort(list_of_students)}
    end)
  end
end

Did you have a different solution or think that my solution could be improved? Please hit me up in the comments below!

Comments