Tracks
/
Elm
Elm
/
Exercises
/
Bandwagoner
Bandwagoner

Bandwagoner

Learning Exercise

Introduction

Records

Records are data structures grouping together related information with labels. They are similar to objects in JavaScript or Java, or structs in C or Rust, but with some key distinctions.

Creating records

Records are created with curly brackets and their elements are separated by commas.

firefly =
    { name = "Firefly"
    , creator = "Joss Whedon"
    , episodes = 14
    }

The type of a record is also defined with a similar syntax, except equal signs = are replaced by colon signs :.

firefly : { name : String, creator : String, episodes : Int }
firefly =
    { name = "Firefly"
    , creator = "Joss Whedon"
    , episodes = 14
    }

Repeating these type definitions is cumbersome, so using a type alias to share the definition is a common idiom.

type alias TvSeries =
    { name : String
    , creator : String
    , episodes : Int
    }

firefly : TvSeries
firefly =
    { name = "Firefly"
    , creator = "Joss Whedon"
    , episodes = 14
    }

The type alias also automatically supplies a constructor function for the record. This is sometimes surprising since functions in Elm must start with a lowercase character, but type constructors are one exception.

firefly : TvSeries
firefly = TvSeries "Firefly" "Joss Whedon" 14

Accessing record fields

The main way to access a field value of a record instance is to use the dot-notation such as firefly.creator. The Elm compiler also supports special accessor functions starting with a dot . such as .creator that will work on any record with a field of that name. That is the second exception to the rule that functions must start with a lowercase character. In fact user-defined functions must start with a lowercase character, but the two examples we just discovered are generated by the compiler, not the programmer.

firefly : TvSeries
firefly =
    { name = "Firefly"
    , creator = "Joss Whedon"
    , episodes = 14
    }

firefly.name
    --> "Firefly"

.creator firefly
    --> "Joss Whedon"

Records can also be destructured in bindings using the record pattern matching. Destructuring works with any record that has fields of the relevant names.

episodesCount : TvSeries -> Int
episodesCount { episodes } =
    episodes

complicatedCopy : TvSeries -> TvSeries
complicatedCopy show =
    let
        { name, creator } = show
    in
    { name = name
    , creator = creator
    , episodes = episodesCount show
    }

Updating records

Records are immutable, as everything in Elm, so once a record is defined, its field values can never change. There is a special syntax to create a copy of an existing record, but changing one or more fields. This syntax uses the pipe symbol | to distinguish the original record and the fields that will change, such as { oldRecord | field1 = newValue }.

firefly : TvSeries
firefly =
    { name = "Firefly"
    , creator = "Joss Whedon"
    , episodes = 14
    }

updatedFirefly : TvSeries
updatedFirefly =
    { firefly | creator = "J Whedon", episodes = 15 }

Comparing records

Elm uses [structural equality][equality], which means that two instances of the same record with identical values are equal.

firefly1 = TvSeries "Firefly" "Joss Whedon" 14
firefly2 = TvSeries "Firefly" "Joss Whedon" 14

firefly1 == firefly2
    --> True

Extensible records

Elm also supports structural typing meaning that if a function requires a record with an x and y field, it will work with any record that has those fields such as 2D points, 3D points, spaceships, etc. Those record types have a pipe | in their definition, such as { a | x : Float, y : Float } and are called "extensible records" in Elm terminology. Beware of the difference between a pipe | in a type definition, which is an extensible record definition ({ a | x : Int }), and a pipe | in an actual record instance which means that we are updating some fields of that record.

point2d = { x = 1, y = 2 }
point3d = { x = 3, y = 4, z = 7 }

.x point2d --> 1
.x point3d --> 3

length : { a | x : Float, y : Float } -> Float
length vector =
    sqrt (vector.x * vector.x + vector.y * vector.y)

length point2d --> 2.236068
length point3d --> 5

translateX : { a | x : Float } -> { a | x : Float }
translateX vector =
    { vector | x = vector.x + 1 }

translateX point2d --> { x = 2, y = 2 }
translateX point3d --> { x = 4, y = 4, z = 7 }

Instructions

In this exercise you're a big sports fan and you've just discovered a passion for NBA basketball. Being new to NBA basketball, you're doing a deep dive into NBA history, keeping track of teams, coaches, their win/loss stats and comparing them against each other.

As you don't yet have a favorite team, you'll also be developing an algorithm to figure out whether to root for a particular team. You have seven tasks to help you develop your proprietary root-for-a-team algorithm.

1. Define the model

Define the Coach type alias for a record with the following two fields, in the following order:

  • name: the coach's name, of type String.
  • formerPlayer: indicates if the coach was a former player, of type Bool.

Define the Stats type alias for a record with the following two fields, in the following order:

  • wins: the number of wins, of type Int.
  • losses: the number of losses, of type Int.

Define the Team type alias for a record with the following three fields, in the following order:

  • name: the team's name, of type String.
  • coach: the team's coach, of type Coach.
  • stats: the team's stats, of type Stats.

2. Create a team

You want to interface with some third party code that has the concept of a team, but which calls your code with the parameters in a different order. Add a createTeam function with the following signature String -> Stats -> Coach -> Team.

3. Replace the coach

NBA owners being impatient, you found that bad team results would often lead to the coach being replaced. Implement the replaceCoach function that takes the new coach and the team as parameters, and returns the team but with the new coach:

let
    coach = { name = "Larry Bird", formerPlayer = True }
    stats = { wins = 58, losses =  24 }
    team = { name = "Indiana Pacers", coach = coach, stats =  stats }
    newCoach = { name = "Isiah Thomas", formerPlayer = True }
in
replaceCoach newCoach team
--> { name = "Indiana Pacers"
--  , coach = { name = "Isiah Thomas", formerPlayer = True }
--  , stats = { wins = 58, losses = 24 } }

4. Check if you should root for a team

In the future, you plan to come up with a complicated algorithm about whether to root for a team or not, but for the first attempt, you have decided that you should root for any team has more wins than losses.

You want this function to be reusable in other contexts, so use an extensible record in the type signature to accept any record that has the relevant fields. To keep the function as concise as possible, you want to use pattern matching to get the stats from the function parameter. Implement the rootForTeam function that takes a team and returns True if you should root for that team; otherwise, return False.

let
    spursCoach = { name = "Gregg Popovich", formerPlayer =  False }
    spursStats = { wins = 56, losses = 26 }
    spursTeam = { name = "San Antonio Spurs", coach = spursCoach, stats = spursStats }
in
rootForTeam spursTeam
--> True
Edit via GitHub The link opens in a new window or tab
Elm Exercism

Ready to start Bandwagoner?

Sign up to Exercism to learn and master Elm with 22 concepts, 90 exercises, and real human mentoring, all for free.