Day 1, April 17, 2020: Typeclasses
Typeclasses are among the most powerful features in Haskell. They allow us to define generic interfaces that provide a common feature set over a wide variety of types; defining a set of functions that can have different implementations depending on the type of data they are given.
Here is a typeclass is defined:
class BasicEq a where isEqual :: a -> a -> Bool isEqual x y = not (isNotEqual3 x y) isNotEqual :: a -> a -> Bool isNotEqual x y = not (isEqual x y)
and here is how it is used (instantiated):
instance BasicEq Bool where isEqual True True = True isEqual False False = True isEqual _ _ = False
and also
instance BasicEq Color where isEqual Red Red = True isEqual Green Green = True isEqual Blue Blue = True isEqual _ _ = False
This is like refl
in Martin-Löf type theory. If BasicEq
or ==
is not implemented for a type, you could say (in the abstract) that that type has no notion of equality since =
is uninhabited.) You can instantiate an existing typeclass for a new type:
instance Show Color where show Red = "Red" show Green = "Green" show Blue = "Blue"
On the Read
and Show
typeclasses:
You may often have a data structure in memory that you need to store on disk for later retrieval or to send across the network. The process of converting data in memory to a flat series of bits for storage is called serialization. It turns out that read and show make excellent tools for serialization. show produces output that is both human- and machine-readable. Most show output is also syntactically valid Haskell, though it is up to people that write Show instances to make it so.
Example
First write a data structure to disk:
ghci> let d1 = [Just 5, Nothing, Nothing, Just 8]::[Maybe Int] ghci> putStrLn (show d1) [Just 5,Nothing,Nothing,Just 8] ghci> writeFile "test" (show d1)
Now read it back:
ghci> input <- readFile "test" "[Just 5,Nothing,Nothing,Just 8]" ghci> let d2 = read input Ambiguous type variable `a' in the constraint: `Read a' arising from a use of `read' at <interactive>:1:9-18 Probable fix: add a type signature that fixes these type variable(s) ghci> let d2 = (read input)::[Maybe Int] ghci> print d1 [Just 5,Nothing,Nothing,Just 8] ghci> print d2 [Just 5,Nothing,Nothing,Just 8] ghci> d1 == d2 True
Automatic derivation
data Color = Red | Green | Blue deriving (Read, Show, Eq, Ord)
Pragmas
{-# LANGUAGE TypeSynonymInstances #-}
Data versus Newtype
data DataInt = D Int deriving (Eq, Ord, Show) newtype NewtypeInt = N Int deriving (Eq, Ord, Show)
When we declare a newtype, we must choose which of the underlying type’s typeclass instances we want to expose. Here, we’ve elected to make NewtypeInt provide Int’s instances for Eq, Ord, and Show. As a result, we can compare and print values of type
NewtypeInt: ghci> N 1 < N 2 True
Since we are not exposing Int’s Num or Integral instances, values of type NewtypeInt are not numbers. For instance, we can’t add them:
ghci> N 313 + N 37 --> error
Further example (hiding implementation):
newtype UniqueID = UniqueID Int deriving (Eq)
The compiler treats UniqueID as a different type from Int. As a user of UniqueID, we know only that we have a unique identifier; we cannot see that it is implemented as an Int.
Recap: defining types
- The
data
keyword introduces a truly new algebraic data type. - The
type
keyword gives us a synonym to use for an existing type. We can use the type and its synonym interchangeably. - The
newtype
keyword gives an existing type a distinct identity. The original type and the new type are not interchangeable.
.