Custom Types and Typeclasses

Up to now we've worked with the basic types: Bool, Int, Char, Maybe, etc. They're awesome, but now its time to venture into making our own types. We can use that data keyword to define a type.

data Bool = False | True

data means that we're defining a new type. The type name, Bool, and each part of the type definition separated by the | is a value constructor. The value constructors define the values that our type can have, while the | is read as or.

The definition of Int:

data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | .... | 2147483647

Int is fairly straightforward, with the first and last values being the upper and lower bounds of the type. The actual definition of Int omits the ellipses for a ton of numbers.

data Shape = Circle Float Float Float | Rectangle Float Float Float Float
ghci> :t Circle
Circle :: Float -> Float -> Float -> Shape
ghci> :t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape

Notes relating to the implementation of record syntax when defining custom types can be found in 07-customTypes.hs

Type parameters

data Car = Car { company :: string
               , model :: string
               , year :: Int 
               } deriving (Show)

A value constructor can take some value's parameters and then produce a new value. For instance, the Car constructor takes three values and produces a new car value. In a similar manner, type constructors can take types as parameters to produce new types.

data Maybe a = Nothing | Just a

a is the type parameter here, and because there is a type parameter invoved, we need to make use of the Maybe type constructor.

Note: no value can have a sole type of Maybe. In our case it could be Maybe String, Maybe Car, Maybe Int, etc.

Type parameters are useful because we can make different types with them depending on what kind of types we want contained within our data type.

A note on Polymorphism

Derived Instances

data Person = Person { firstName :: String
                     , lastName :: String
                     , age :: Int
                     } deriving (Eq, Show, Read)
ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> mikeD
Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> "mikeD is: " ++ show mikeD
"mikeD is: Person {firstName = \"Michael\", lastName = \"Diamond\", age = 43}"

Read is pretty much the inverse typeclass of Show. Show is for converting values of our type to a string. Read is for converting strings to values of our type.

Type synonyms

When looking at the various types, we can see that [Char] and String are equivalent and interchangable types. This behavior is implemented with Type Synonyms. Type synonyms don't really do anything, though. It just allows us to provide different names for our types.

type String = [Char]
phoneBook :: [(String,String)]
phoneBook =
  [("john", "555-1234") 
  ,("jane", "555-5678")
  ]

-- declaring this would allow us to change our type definition for phoneBook to phoneBook :: PhoneBook
type PhoneBook = [(String,String)]

-- declaring the type synonyms below would allow us to change our definition for PhoneBook to type PhoneBook = [(Name,PhoneNumber)]
type PhoneNumber = String
type Name = String

Defining type synonyms is something that Haskell programmers do to when they want to convey more information about what strings in their functions should be used and what they represent.

-- that makes for a much easier to read type definition. otherwise it would have been
-- inPhoneBook :: String -> String -> [(String,String)] -> Bool
inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool
inPhoneBook name pnumber pbook = (name,pnumber) `elem` pbook

When takling about concrete types, we mean fully applied types like Map Int String, or when dealing with polymorphic functions, [a] or (Ord a) => Maybe a

Just like we can partially apply functions to get new functions, we can partially apply type parameters and get new type constructors from them.

type IntMap v = Map Int v

or

type IntMap = Map Int

Make sure you understand the distinction between type constructors and value constructors.

Another cool data type that takes two types as its parameters is the Either a b type. This is roughly how it is defined:

  data Either a b = Lefft a | Right b deriving (Eq, Ord, Read, Show)

If it has two value constructors, Left is used and its contents are of type a. If Right is used, then its contents have type b. We can then use this type to encapsulate a value of one type or another and then when we get a value of type Either a b, we usually pattern match on both Left and Right.

Recursive data structures

Think about the following list: [5]

What's going on here? This is just a syntactic synonym for 5:[].

The left-hand portion of the :, we have a value, and on the right an empt list. What about [4,5]? That's analagous to 4(5:[]). The pattern repeat for each item in the list.

Cons === :

data List a = Empty | Cons { listHead :: a, listTail :: List a } deriving (Show, Read, Eq, Ord)

Phantom types Phantom types are a rather simple concept, but a powerful one at that. They serve as the foundation for a lot of other interesting type-level manipulations that can make Haskell an extremely interesting language. The basic idea for a phantom type is simple; it's a type parameter that isn't actually used to represent a given runtime value.

newtype Id a = Id Text

This type represents an id for some kind of value. The kind of value is specified as the type parameter, a, but it isn't used anywhere. No matter what a is, Id is just a piece of text. This makes it possible to write functions that operate on specific kinds of ids, and those invariants will be statically checked by the compiler, even through the runtime representation is entirely identical.

fetchUser :: MonadDB m => Id User -> m User

Implementing a binary search tree

Sets and maps from Data.Set and Data.Map are implemented when using trees. The only difference is that instead of being a normal tree, they used balanced binary search trees, which are always balanced. Right now we'll just be implementing a normal binary search tree, though.

data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

What's going on here? Instead of manually builing a tree, we're going to make a function that takes a tree and an element and then inserts an element. We can do this by comparing the value we want to insert into the root node and then if its smaller, we go left, if it's larger, we go right. We do the same for every subsequent node until we reach an empty tree. Once we reach an empty tree, we just insert a node with that value inserted into an empty tree.

singleton :: a -> Tree a
singleton x = Node x EmptyTree EmptyTree

treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert x EmptyTree = singleton x
treeInsert x (Node a left right)
    | x == a = Node x left right
    | x < a  = Node a (treeInsert x left) right
    | x > a  = Node a left (treeInsert x right)

The singleton function is just a shortcut for making a node that has something and two empty sub-trees. In the insertion function, we are presented with the edge condition as a pattern.

Typeclasses 102

A quick recap on typeclasses: typeclasses are like inferences.

A typeclass defines some behavior, and then types that can behave in that way are made instances of that typeclass. For example, the Eq typeclass is for stuff that can be equated. It defines the functions == and /=. As an example, if we have a type Car that compares two cars with the equality function ==, then it makes sense for Car to be an instance of Eq.

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)

Above we defined the Eq typeclass. Let's break it down. Firstly, class Eq a where means that we're defining a typeclass that's called Eq that takes a type variable of a. We then write type declarations for both equality functions along with function definitions.

You can also make typeclasses that are subclasses of other typeclasses. The class declaration for Num is a bit long, but here's the first part.

class (Eq a) => Num a where
    ...

What the definition above states that for any given type a, it must be an instance of Eq. This essentially means that type a becomes an instance of Eq prior to becoming an instance of Num.

Looking back at the Eq typeclass definition above, we see that a is used as a concrete type because all of the types in function have to be concrete. That's why we can't do something like

instance Eq Maybe where
    ...

This is because, like we've seen, a has to be a concrete type, but Maybe isn't a concrete type. It's a type constructor that takes a parameter and then returns a concrete type.

A yes-no typeclass

In JavaScript and other weakly typed languages, you can put almost anything inside of an expression. Even though strictly using Bool for boolean semantics works better in Haskell, we can implement it's JavaScript-like behavior in Haskell. Let's start out with a class declaration.

class YesNo a where
    yessno :: a -> Bool

Next up, let's define some instances. For numbers, we'll assume that any number that isn't 0 is true-ish and 0 itself is false-ish.

instance YesNo Int where
  yesno 0 = False
    yesno _ = True

Empty lists, and by extension strings, are a no-ish value, while non-empty lists are a yes-ish value.

instance YesNo [a] where
  yesno [] = False
    yesno _ = True
``

A `Bool` instance.

```hs
instance YesNo Bool where
  yesno = id

How about a Maybe a instance?

instance YesNo (Maybe a) where
  yesno (Just _) = True
    yesno Nothing = False

Previously, we defined a Tree a type, that represeneted a binary search tree. We can say that an empty tree is false-ish and anything that's not an empty tree is true-ish.

instance YesNo (Tree a) where
  yesno EmptyTree = False
    yesno _ = True
ghci> yesno $ length []  
False  
ghci> yesno "haha"  
True  
ghci> yesno ""  
False  
ghci> yesno $ Just 0  
True  
ghci> yesno True  
True  
ghci> yesno EmptyTree  
False  
ghci> yesno []  
False  
ghci> yesno [0,0,0]  
True  
ghci> :t yesno  
yesno :: (YesNo a) => a -> Bool

We can now write a function that mimics the if statement, but works with YesNo values.

yesnoIf :: (YesNo y) => y -> a -> a -> a
yesnoIf yesnoVal yesResult noResult = if yesno yesnoVal then yesResult else noResult
ghci> yesnoIf [] "YEAH!" "NO!"  
"NO!"  
ghci> yesnoIf [2,3,4] "YEAH!" "NO!"  
"YEAH!"  
ghci> yesnoIf True "YEAH!" "NO!"  
"YEAH!"  
ghci> yesnoIf (Just 500) "YEAH!" "NO!"  
"YEAH!"  
ghci> yesnoIf Nothing "YEAH!" "NO!"  
"NO!"

results matching ""

    No results matching ""