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.
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!"