% Modeling data in Haskell % Chris Allen % April 30, 2015
Haskell has a nice type system
-
Lets try to make proper use of it
-
So no more of this:
whoKnows :: String -> Map String String -> IO ()You can't reason about it.
Haskell datatype syntax
Nullary constructor:
data Trivial = Trivial
-- [1] [2]-
Type constructor
-
Data constructor - takes no arguments, thus "nullary"
Constructors?
We use type constructors to refer to types by name in type signatures.
f :: String -> String -> String
f = ...doesn't matta...Here we refer to the type String three times in our type signature, denoted syntactically by the double colon ::
We use data constructors to create values. There's some special syntax for built-in types like String, List, tuples, but in most cases you'll use a data constructor explicitly introduced by the datatype.
So which is which?
data TrivialTypeConstructor
= TrivialDataConstructorRule of thumb: before the = is the type constructor, after the = are the data constructors.
If there's a single data constructor, it'll often have the same name as the type constructor because types and values are strictly separated in Haskell.
How do we use our datatype with a single nullary data constructor?
theProofIs :: Trivial
theProofIs = Trivial
trivialityBegets :: Trivial -> Trivial
trivialityBegets Trivial = Trivial
-- alternately
trivialityBegets _ = Trivial
-- or
trivialityBegets x = TrivialHaskell datatype syntax
Unary constructor (takes one argument):
data Identity a = Identity a
-- [1] [2]-
Type constructor, takes one argument.
-
Data constructor, takes one argument. Thus, "unary". Unary/nullary refers to the data constructor. You'll see examples with multiple data constructors of mixed arity later.
How do we use Identity?
unpack :: Identity a -> a
unpack (Identity a) = a
embed :: a -> Identity a
embed a = Identity a
imap :: (a -> b) -> Identity a -> Identity b
imap f (Identity a) = Identity (f a)Identity doesn't do much, so if this seems pointless, you're not missing anything.
Type constructor has to agree with data constructor
Why can't we have:
data Identity = Identity aBecause you'll get the error: Not in scope: type variable ‘a’
Without the argument existing for the data and the type constructor, we have no means of expressing what we think Identity contains. There ways to "hide" the type variables in data constructors from the type constructors but that's for another day.
Product
What happens if you add another argument to a unary data constructor? Products!
data Person = Person String IntProduct here can also be read to mean, "record" or "struct", but be careful with assumptions about representation. Here Person is a product of a String and an Int.
Person with record syntax
data Person = Person { name = String
, age = Int }Person defined using record syntax for the fields.
Using the field accessors:
getName :: Person -> String
getName p = name p
-- eta reduce
getName = name
-- redundantTuples
Tuples are our "anonymous product", so called because we don't name anything. We could rewrite our Person type as:
type Person = (String, Int)The type keyword only creates type constructors, that is, aliases to other types with their own data constructors. You could refer to this value ("blah", 3) has having type Person.
Nesting tuples
You can also nest tuples. Given the product of String and Int and Integer and another String you could write that as:
(String, (Int, (Integer, String)))This is what makes the 2-tuple an anonymous universal product, that we can nest them.
Exercises
Rewrite the following type into a nested two tuple:
data Car = Car {
make :: CarMake
, model :: CarModel
, year :: CarYear
}turns into:
type Car = ???Exercises
Given the functions:
fst :: (a, b) -> a
snd :: (a, b) -> bAdd the accessors back for your nested tuple type.
make :: Car -> CarMake
make = undefined
model :: Car -> CarModel
model = undefined
year :: Car -> CarYear
year = undefinedSum type
The Bool datatype is defined as follows:
data Bool = False | TrueWhat we've done here is made it so two different data constructors are values of type Bool. Where product ~ and, sum ~ or.
We have an anonymous sum type too
data Either a b = Left a | Right bGettin' silly
We could atomise Bool like so:
data False' = False' deriving Show
data True' = True' deriving Show
type Bool' = Either False' True'Bonus
It'll even type-check that you're not messing the order up:
Prelude> Right False' :: Bool'
<interactive>:57:7:
Couldn't match expected type ‘True'’
with actual type ‘False'’
In the first argument of ‘Right’,
namely ‘False'’
In the expression: Right False' :: Bool'
Prelude> Right True' :: Bool'
Right True'
Making the "algebra" in algebraic data types do work
- There's an actual set of operations here.
data Bool = False | True- False = 1
- True = 1
- | = +
- Either also = +
data Bool = False + True
data Bool = 1 + 1
Bool = 2 inhabitantsMaking the "algebra" in algebraic data types do work
type Bool' = Either False' True'
Either = +
data False' = False'
False' = 1
True' = 1
type Bool' = Either 1 1
= 1 + 1
-- same as ordinary Bool
-- we can say they are equivalentMaking the "algebra" in algebraic data types do work
Knowing how big your domain is important for knowing how comprehensible it is, as well as knowing how it relates
(,) = *
type DoesntMatter = (Bool, Bool)
type DoesntMatter = Bool * Bool
type DoesntMatter = 2 * 2
type DoesntMatter = 4 inhabitantsExercises
How many inhabitants does each type have?
-- Word8 = 0-255
import Data.Word
type A = Either Word8 Bool
type B = (Word8, Bool)
type C = Either (Bool, Word8) (Word8, Bool)Don't do this
data CarType = Null |
Car { carid :: Int
, position :: Float
, speed :: Float,
, carLength :: Float
, state :: [Float]
} deriving (Show,Eq)Why?
Because it's redundant, obnoxious, and it introduces partial functions. Don't mix record syntax and sum types!
Partial what? Partial functions are functions that have inputs for which they don't have answers.
data Example = Null
| Example {
blah :: Int
} deriving Show
Prelude> blah $ Example 10
10
Prelude> blah $ Null
*** Exception: No match in record selector blah
Pls no.
Maybe exists, use it!
data Maybe a = Nothing | Just aIf you have a function that might not be able to return a sensible CarType, return Maybe CarType!
Cleaning up our datatypes
This isn't great.
data Hero =
Hero {
class :: String
, race :: String
, statusEffects :: [String]
, inventory :: Map String Int
} deriving ShowYou're not getting a lot of mileage out of the type system when you do this.
Exercise
Tell me how to fix the Hero datatype.