Lab 9. Algebraic Data Types

Types are an essential component in haskell. They start with capital letters, like Int. We have multiple ways of customizing data types.

The first one is type aliases, signaled by the keyword type. These allow us to rename complex types to simpler types. They are used for better clarity.

type Matrix = [[Int]]
type StudentName = String

The second one is data. This enables us to create new data types. These types can take parameters. Using | allows us to define a sum type (a type with multiple constructors). Take a look at Maybe a (this is a type that already exists in haskell). We can do a lot of things with these, including pattern matching.

data BinaryDigit = Zero | One
data Maybe a = Just a | Nothing
data List a = Cons a (List a) | Void
data Both a b = Both a b

A particular case of data is newtype. This is an optimization for single-parameter single-constructor types. Without going into details, this is used to better expose the contained type. We can think of data constructors like pointers (that's how they are implemented). Newtype replaces the pointer with the uderlying value, leading to less memory access and increased performance.

newtype WithIndex a = WithIndex (a, Int)

For practical usecases, we might need a way to model more complex data. In other languages, this can be achieved using structs, classes, interfaces etc. Haskell allows us to model data using records. A record is a specific type of data very similar to a struct. We use deriving Show as a way to be able to convert the type to string when printing it in the terminal.

data Dog = Dog{ name :: String
              , breed :: String
              , age :: String
              } deriving Show

Now, we can create a Dog like this.

myDog = Dog {name="Spike", breed="Golden Retriever", age = 5}

How can we access the dog's information? Simple. Haskell implements getters for us by default

name myDog --"Spike"
breed myDog --"Golder Retriever"
age myDog --5

Typeclasses are an important concept in haskell. They are very similar in concept to Java interfaces (we implement certain behaviours through them). For example, we might want to convert an entity to a string to print it. This could be done by implementing the Show class. Take a look at its implementation. Notice the {-# MINIMAL showsPrec | show #-}. That tells us that it's enough to implement one of those functions to have a complete implementation. Let's implement show for Maybe a

-- The syntax "Show a =>" is called a constraint. This tells us that a also needs to be an instance of Show. Why do we need it here? 
instance Show a => Show (Maybe a) where
    show Nothing = "Nothing"
    show (Just x) = "Just "  ++ show x

In summary, typeclasses allow us to abstract certain behaviours. Some important ones we'll use in this lab are Eq(to specifiy whether 2 things are equal or different) or Ord, to be able to compare type instances.

After the lab, you can take a look at some more cool stuff related to data types in haskell and functional types in general
Unboxed types
Lenses

9.1.1 Let's implement a ChessResult data type. In chess, a game result can be either a Win, a Draw or a Loss.

data ChessResult = ???

9.1.2 We want to be able to show a ChessResult. For this, we could do deriving Show to let GHC derive the implementation, but let's do it ourselves.

instance Show ChessResult where
    ???

9.1.3 In chess, your performance is rated based on a point system. A win is equivalent to a point, a draw is 0.5 points and a loss is 0 points

points :: ChessResult -> Float
points = ???

9.1.4 We want to find out the number of point a player earns after playing multiple games. Try to implement this as a closure.

score :: [ChessResult] -> Float
score = ???

9.1.5 Now that we can measure a player's score, we want to establish a ranking. Return the descending list of scores. Hint: sort

ranking :: [[ChessResult]] -> [Float]
ranking = ???

9.1.6 Each tournament also offers a cash prize, based on player performance. Compute the earnings for each player. The formula we'll use is player_earnings = player points * prize money / total points

earnings :: [[ChessResult]] -> Float ->[Float]
earnings playerScores prizeMoney = ???

9.2.0 We want to model a chess player using records. We care about 3 things: name, elo(a floating point value which measures a player's strength) and a list of games played (name it tournamentGames). We also want to model a chess tournament. We'll represent it as a binary tree.

data Player = Player { ??? }
data Tree a = ??? deriving Show
type Tournament = Tree Player

9.2.1 We want to be able to compare players based on their performance. In order to do this, we'll have to implement 2 type classes

-- Two players are considered equal if their score is the same. Alternatively, 2 players are different if their scores are different
instance Eq Player where
  ???
 
-- In the same manner, players can be compared based on their score
instance Ord Player where
  ???

9.2.2 After a game, we want to update a player's games with the new result.

addResult :: ChessResult -> Player -> Player
addResult = ???

9.2.3 It's finally time to play some chess. After the game is played, we want to return 2 players with updated results. The first one should be the winner (helpful later). However, everyone knows chess is a luck-based game. As such, a player will win a game if their odds of winning (calculated based on elo difference) is greater than 0.5.

-- odds that A beats B based on their elo
winningOdds :: Float -> Float -> Float
winningOdds eloA eloB = 1 / (1 + 10** ((eloB-eloA)/400))
 
playGame :: Player -> Player -> (Player, Player)
playGame player1 player2 = ???

9.2.4 A guy with a lot of free time decided it would be fun to play games against multiple opponents. Return the updated player and the updated list of opponents (take a look at mapAccumL).

multipleMatches :: Player -> [Player] -> (Player, [Player])
multipleMatches = ???

9.2.5 It seems that the chess community is boiling and players are ready to throw hands. Each player must face all other players in a single match. At the end, all players will have their results updated.

groupStage :: [Player] -> [Player]
groupStage = ???

9.2.6 Playing every player is a bit boring. We want to organize a tournament to decide the best player faster. We can assume each tournament will have 2^n players for simplicity. Each round, groups of 2 players will face each other, with the winner advancing to the next round. Don't forget to update their results.

tournamentStage :: [Player] -> Tournament
tournamentStage = ???

9.2.7 Carlos Magnusen wants to organize a chess tournament. As he only wants the best players to participate, he decided to organize a big group stage with all players facing each other a tournament stage with the best 2^n groups players (n must be as high as possible - if 150 players are playing, we expect n to be 7).

chessTournament :: [Player] -> Tournament
chessTournament = ???

9.2.8 Carolina wants to see how her beloved Romeo did. However, he sometimes goes to the 'chess' club instead of playing, so he may have not been present at all. Implement the function findPlayerScoreByName, which returns Just score if he was there or Nothing if he wasn't

findPlayerScoreByName :: String -> Tournament -> Maybe Float
findPlayerScoreByName = ???