This is an old revision of the document!


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 type keyword is used to introduce type aliases (new names for already existing types). Its main role is to improve the clarity of type annotations by renaming complex types to simpler ones or providing additional context.

type Matrix = [[Int]]
type Name = String
type PhoneNumber = String
PhoneBook = Map Name PhoneNumber

We can also create new types using the data keyword. Each type has one or more constructors (separated by | in the type's definition). Each constructor can take parameters, and the type itself may take type parameters (enabling the creation of generic types).

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

To create objects of the given types, we use the constructors as if they were functions:

bDigitOne :: BinaryDigit
bDigitOne = One
maybeFive :: Maybe Int
maybeFive = Just 5
myIntList :: List Int
myIntList = Cons 1 (Cons 2 Void) 

Pattern matching can be used on any types defined with data.

listHead :: List a -> a
listHead (Cons x _) = x
listHead Void = undefined

Additionally, we can use records to give names to specific paraneters of constructors:

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

Using this syntax allows us to create objects like this:

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

Additionally, it defines a 'getter' function for each named field:

name myDog -- "Spike"
breed myDog -- "Golden Retriever"
age myDog -- 5
The record syntax can be used for types with multiple constructors too. In this case, constructors can also share parameter names as long as the types of parameters with the same name match. Example:
data Expr = Atom Int | Add {left:: Expr, right:: Expr} | Subtract {left:: Expr, right: Expr} | ... -- here, 'left' from 'Add' needs to have the same type as 'left' from 'Subtract'

Another, more specialised, way of creating new types is the newtype keyword. It is used to create single-constructor, single-parameter types with some memory usage and access optimisations.

newtype WithIndex a = WithIndex (a, Int)

implementation details about newtype

implementation details about newtype

Let's take the following two types as examples:

data A = A Int
newtype B = B Int

data constructors are implemented as a set of pointers to the underlying parameters, so an object of type A is essentially a pointer to an Int. Accessing the underlying Int requires an indirect memory access, and we need sapce allocated for both the Int and the pointer.

newtype is represented directly by the underlying type, leading to the elimination of the indirect memory access and removes the additional memory requirement to store the pointer.

Type classes represent a similar concept to Java interfaces. They are used to group together types which have a certain behaviour. For example, all types which can be converted to a String and printed to the console belong to the Show type class.

Do not confuse type classes with the classes from the Object-Oriented paradigm. Type classes are a category of types, while OOP classes are a category of objects.

We can define a class as such:

class MyShow a where
    myShow :: a -> String

To enroll a type in this class we create an instance for it:

data BinaryDigit = One | Zero
 
instance MyShow BinaryDigit where
    myShow One = "1"
    myShow Zero = "0" 

Type classes allow us to place restrictions on parameter types in function definitions, other type class definitions and instaces:

showTable :: Show a => [[a]] -> String -- this function can show any table-like list-of-lists as long as the type of the elements is showable itself
showTable table = ...  -- we can freely use 'show' on each "cell" of the table 
 
class  (Eq a) => Ord a  where -- any type 'a' belonging to this class also needs to belong in the 'Eq' class 
  (<), (<=), (>=), (>)  :: a -> a -> Bool
  max, min              :: a -> a -> a
 
instance (Show a) => Show [a] where -- we define an instance for lists of all showable types
    show = ...

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

We wish to model a tournament scoring system for 1v1 games such as chess (we will not be referring to the logic of the game itself, only the scorekeeping system). The tournament will have two stages:

  • Group stage: Players are placed into $ 2^n$ groups. Each player plays against all other players in the same group. The top 2 players will advance to the elimination stage.
  • Elimination stage: Players are paired up to play against each other. For each matchup, the loser gets eliminated, the winner remains. The process is repeated until a single player remains.

9.1.1. Define the datatypes we will need:

data MatchResult = ??? -- the result of a match could be a Win, Loss or Draw
data Player = Player {???} -- we care about 3 things: the player's name, elo (floating point which measures the player's strength), and a list of games played (more specifically, their results)
data Tree a = ??? deriving Show -- A binary tree we will use to represent the elimination stage of the tournament
type Tournament = Tree Player

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

instance Show MatchResult where
    ???

9.1.3. We will want to update player match history after each match.

addResult :: MatchResult -> Player -> Player
addResult = ???

9.1.4. During the group stage of the tournament, we will require a point-based scoring system. A loss is worth 0 points, a win is worth 1, and a draw 0.5.

points :: MatchResult -> Float
points = ???

9.1.5. To determine the best players from each group, we will need to determine how many points each player scored.

score :: Player -> Float
score = ???

9.1.6. Finally, for the group stage we will need to sort the players to establish a ranking. The easiest way to do this is to define a order relationship for players, based solely on score (2 players are equal if their scores are equal, similar for greater than and less than relationships)

instance Eq Player where
  (==) = ???
 
instance Ord Player where
  (<=) = ???
  -- haskell can infer implementations for all other operations if the implementation for (<=) is given. 

9.1.7. For simplicity, we will 'simulate' matches between players by looking at their elo. The player with more elo always wins.

playGame :: Player -> Player -> (Player, Player)
playGame player1 player2 = ???

TODO:

Simulate the groups and elimination phases. Code skeleton ? Sample players (so they can test things faster).