Haskell language extensions each add something new to the language. Unlike most languages where such extensions would typically be discouraged for reasons of support, stability or unstable API, many haskell extensions have been around for a while, are used often, and will probably make it into some future revision of the Haskell language spec.
Let's take a look at some of the haskell extensions out there and see what they are good for. This is going to be code heavy, as it is mostly a translation of example code and comments to structured blog post, and I expect I'll update it as I come across more extensions I find interesting!
Go here to get all of the code used below, ready for you to load straight into GHCI and have a play with. Tested using GHC 7.10.1
ViewPatterns
View patterns let you run arbitrary computations anywhere you can use a pattern match, performing the actual pattern match on the result of this computation. Lets see what this looks like:
someList = [("london",10), ("paris",12), ("sydney",4)]
findIn = flip lookup
-- no view patterns: extract a value from the map, using default if it's not there.
getOrDefault name = case mRes of
Just val -> val
Nothing -> 5
where mRes = findIn someList name
-- view patterns: applies input argument to "findIn someList" and looks to see
-- whether the result matches the pattern Just val. falls to next case if not.
getOrDefault' (findIn someList -> Just val) = val
getOrDefault' _ = 5
View patterns can also make use of variables declared before them in the list of arguments to a function. In this case, the list
variable:
-- no view patterns: take in list as well.
getOrDefaultList list name = case mRes of
Just val -> val
Nothing -> 5
where mRes = findIn list name
-- view patterns: partial application can use prior arguments as well:
getOrDefaultList' list (findIn list -> Just val) = val
getOrDefaultList' _ _ = 5
They can be arbitrarily nested and exist anywhere a pattern match can:
-- no view patterns: use list above to convert values to integers and
-- sum them up. 0 if not found in list.
-- eg. sumList ["london","paris"] == 22
sumList (a : as) = case mRes of
Just val -> val + sumList as
Nothing -> sumList as
where mRes = findIn someList a
sumList [] = 0
-- view patterns simplify it a little:
sumList' ((findIn someList -> Just val) : as) = val + sumList as
sumList' (_ : as) = sumList as
sumList' _ = 0
PatternSynonyms
Pattern synonyms allow you define aliases for pattern matches. They allow you to abstract away details of your data structure without introducing the extra overhead of converting to other types, and play nice with view patterns too! First we'll declare a few basic types and functions to play with:
data Day = Sunday
| Monday
| Tuesday
| Wednesday
| Thursday
| Friday
| Saturday
deriving (Eq,Show)
data Time = Time {
hour :: Int,
minute :: Int
} deriving (Eq,Show)
data DayTime = DayTime {
day :: Day,
time :: Time
} deriving (Eq,Show)
-- print the time, padding it.
printTime :: Time -> String
printTime (Time h m) = pad (show h)++":"++pad (show m)
where pad l@(_:_:_) = l
pad l@(_:_) = "0"++l
-- a function to print the day and time given a DayTime:
printDayTime :: DayTime -> String
printDayTime (DayTime d t) = "It's "++show d++", "++printTime t
Now, lets create some basic pattern aliases:
-- some simple patterns to act as aliases for certain DayTimes:
pattern SundayNoon = DayTime Sunday (Time 12 00)
pattern MidnightFriday = DayTime Friday (Time 00 00)
Making use of these in our console:
Main> printDayTime SundayNoon
"It's Sunday, 12:00"
Main> printDayTime MidnightFriday
"It's Friday, 00:00"
We can use variables in patterns as well:
pattern Sun h m = DayTime Sunday (Time h m)
pattern Mon h m = DayTime Monday (Time h m)
These can be used as follows:
Main> printDayTime (Sun 15 30)
"It's Sunday, 15:30"
So far, there has been a one to one mapping between pattern and data. Thus, we can treat patterns as if they were data. Sun 12 00 == SundayNoon == DayTime Sunday (Time 12 00)
. However, some patterns are one way; one pattern could match many values. One way patterns use <-
instead of =
to denote this.
-- match any DayTime that is sunday or monday using these:
pattern AnytimeSun <- DayTime Sunday _
pattern AnytimeMon <- DayTime Monday _
-- a function that prints the day when given some DayTime value:
whichDay AnytimeSun = "Sunday!"
whichDay AnytimeMon = "Monday!"
whichDay _ = "Some other day!"
These patterns no longer represent a value but a range of values - anything that matches them - so we can use them in place of function parameters to destructure them, but we cant use them in place of values.
main> printDayTime AnytimeSun
BZZZT! error! what time should I print??
We can match the whole daytime as in other pattern matches:
whichDay' d@AnytimeSun = "Sunday "++printTime (time d)
whichDay' d@AnytimeMon = "Monday "++printTime (time d)
whichDay' _ = "Other."
One way patterns can also hand back data in variables which might look a little nicer, especially with complex matches. Here we give t
back so that we can make use of it when we destructure someting:
pattern SunT t <- DayTime Sunday t
pattern MonT t <- DayTime Monday t
whichDay'' (SunT t) = "Sunday, "++printTime t
whichDay'' (MonT t) = "Monday, "++printTime t
--we can of course mix and match pattern synonyms and normal
--destructuring too. first success wins as normal:
whichDay'' (DayTime _ t) = "Other day, "++printTime t
One of the really cool things about PatternSynonyms
is that they can be used in conjunction with ViewPatterns
to run arbitrarily complex functions at match time. Here we run isMorningHour
and see whether the answer matches True
:
isMorningHour t = t < 12
pattern Morning <- DayTime _ (Time (isMorningHour -> True) _)
timeOfDay Morning = "It's morning"
timeOfDay _ = "It's not morning"
View patterns can also give back arbitrary variables. here we expect a tuple of (True,h)
back, where we allow h
to be made use of at match time.
isMorningHour' h = (h < 12, h)
pattern MorningT h m <- DayTime _ (Time (isMorningHour' -> (True,h)) m)
timeOfDay' (MorningT h m) = "It's morning, "++show h++":"++show m
timeOfDay' _ = "It's not morning"
FlexibleInstances
Allows you to define typeclass instances in a more flexible way. Let's look at the problem they solve by setting up a simple class with a few instances.
-- a simple class for seeing whether something is "truthy"
class Truthy a where
truthy :: a -> Bool
instance Truthy Bool where
truthy = id
instance Truthy Char where
truthy c = not $ elem c [' ','\n','\t']
instance Truthy Int where
truthy i = i /= 0
Without flexible instances, we could not do:
instance Truthy String where
truthy s = length s /= 0
We'd get told that instance declarations need to take the form T a1..an
where a1..an
are type variables, but String == [Char] == [] Char
, and Char
is not a type variable but a fixed type. So basically, [] a
would be fine, but [] Char
would not. FlexibleInstances
removes this restriction, and more generally adds flexibility to the format that your instances can take.
Avoiding FlexibleInstances
(in some cases).
An alternate way around the above issue is to enforce that a
has to be Char
via some constraint, for example:
class CharType a
instance CharType Char
instance CharType a => Truthy [a] where
truthy s = length s /= 0
If you wanted to define several instances for lists of different things without FlexibleInstances
, you'd end up delegating the behaviour with regard to the items in the array to another (or the same) typeclass's instances for those individual items, for example:
class Count a where
countLen :: a -> Int
instance Count Char where
countLen a = 1
instance Count Int where
countLen i = i
instance Count a => Count [a] where
countLen as = foldl' (\sum a -> sum + countLen a) 0 as
Here, we treat the items in the array polymorphically by requiriing that they also be instances of Count
, and fold over them to sum up the results in either case. Usage:
main> countLen [1::Int,2,3,4]
10
main> countLen "hello"
5
List types are still bound by the general Count [a]
instance here. However if needbe one can delegate all of the behaviour to a separate typeclass for handling, in this example different []
types:
class Count a where
countLen :: a -> Int
-- catches all array types and delegates
-- to CountArr based on contained type:
instance CountArr a => Count [a] where
countLen as = countLenArr as
-- handle different underlying types:
class CountArr a where
countLenArr :: [a] -> Int
instance CountArr Char where
countLenArr as = length as
instance CountArr Int where
countLenArr as = sum as
Here, running countLen ([1,2,3] :: [Int])
for example results in a call being made to the countLenArr
instance for Int
, and running countLen "hello"
the countLenArr
instance for Char
, each which could have completely independant handling.
MultiParamTypeClasses
Simply, type classes with more than one param allowed (or none!).
-- typeclass with 2 params, a and b.
class LooseEq a b where
looseEq :: a -> b -> Bool
-- now we can define instances for both
instance LooseEq Int Float where
looseEq a b = a == round b
instance LooseEq Float Int where
looseEq a b = round a == b
Usage:
main> looseEq (10.2 :: Float) (10 :: Int)
True
main> looseEq (10 :: Int) (10.2 :: Float)
True
Notice that we have to provide types otherwise the compiler won't know which instance to make use of. Even if there is only one instance, the assumption is that more could be created, and so probably to avoid that changing existing behaviour, the compiler is pretty cautious.
Often with MultiParamTypeClasses
you'll also end up wanting to use FunctionalDependencies
, which can ease the burden on adding type annotation.
FunctionalDependencies
For use in multi param type classes; functional dependencies provide a way to state that one type is dependant on others, so that the compiler can infer it without you having to explicitly tell it. Let's see why we need this:
class Adder' a b c where
add' :: a -> b -> c
instance Adder' Int Int Int where
add' a b = a + b
This will fail to type check:
add' (1::Int) (2::Int)
The problem is that at any point, someone could define an instance of Adder'
with, say, Int Int Double
as its 3 params. To play it safe, the type checker requires then that you explicitly provide the final type to prevent future instances from breaking things. Thus, this is fine but pretty ugly:
add' (1::Int) (2::Int) :: Int
Functional dependencies add a little syntax in the class definition to allow us to avoid this issue:
class Adder a b c | a b -> c where
add :: a -> b -> c
instance Adder Int Int Int where
add a b = a + b
This says that the type c
is uniquely determined from the types a
and b
. This means that there cannot exist an instance with the same types a
and b
but different type c
(for instance, Adder Int Int Double
). As such, the compiler is safe to infer what c
will be based on what it knows a
and b
to be, and so
add (1::Int) (2::Int)
now works.
OverlappingInstances
In actuality, OverlappingInstances
has been depracated in GHC 7.10. Instead, we gain a few pragmas that allow us to achieve the same but allow us to enable overlap on a per instance basis rather than globally. Let's look back at our LooseEq
class from earlier to see why we might need this. Given that we've defined specific instances for LooseEq
already, we could try to define a more general one that works on multiple numbers types. First, a supporting class and instances:
-- a quick class to convert numbers to integers.
class Num a => ToInteger a where
numToInteger :: a -> Integer
instance ToInteger Int where
numToInteger i = toInteger i
instance ToInteger Double where
numToInteger d = round d
instance ToInteger Float where
numToInteger f = round f
Making use of this, we declare a more general instance of LooseEq
as follows:
instance {-# OVERLAPPABLE #-} (ToInteger a, ToInteger b) => LooseEq a b where
looseEq a b = numToInteger a == numToInteger b
-- These overlap the more general case so don't need notation unless they too
-- will be overlapped eg by "LooseEq Int Bool":
instance (ToInteger a) => LooseEq a Bool where
looseEq a b = (numToInteger a /= 0) == b
instance (ToInteger a) => LooseEq Bool a where
looseEq b a = (numToInteger a /= 0) == b
-- this requires FlexibleInstances but doesnt overlap with anything else:
instance (LooseEq a b, Foldable f) => LooseEq (f a) (f b) where
looseEq fa fb = foldl' cmp True (zip (toList fa) (toList fb))
where toList = foldr (:) []
cmp False _ = False
cmp _ (a,b) = a `looseEq` b
-- this is more specific than above and so I use OVERLAPPING to say it's OK:
instance {-# OVERLAPPING #-} (LooseEq a b) => LooseEq [a] [b] where
looseEq as bs = length as == length bs
Notice the language pragmas OVERLAPPING
and OVERLAPPABLE
. Without these, we'd get an overlap error when we tried to make use of these instances, as they overlap with others. In other words, for some use of looseEq
there might be more than one instance that matches (ranging from a more general one to a more specific one).
The three pragmas of interest are:
OVERLAPPABLE
, which when applied to a more general instance means that more specific instances (so, those declared earlier) will be fine.OVERLAPPING
, which when applied to a more specific instance means that it is allowed to be overlapping a more general one.OVERLAPS
, which is equal to adding both of the above.
Use of these pragmas is potentially dangerous because you could be relying on some instance in your code, and then later overlap it with a more specific instance that still matches your usage of it but potentially behaves completely differently. It would seem to be less risky to use the OVERLAPPING
pragma than the OVERLAPPABLE
one, since the former prevents others from creating an overlapping instance without acknowledging it as such, but the latter would not kick up a fuss about it and so potentially alter behaviour silently.
ExistentialQuantification
Allows you to remove types variables on the left of a data
declaration. This is made useful by the fact that you can then constrain types variables on the right hand side to belonging to certain classes (otherwise, you couldnt interact with them at all). Here's the canonical example (where the Typeable
constraint is used only for casting later and is not necessary here):
-- we want to store anything "showable" in some type (Typeable not required yet):
-- notice the lack of 'a' on the left, and instead a 'forall a' on the right to bring
-- 'a' into scope, and then a class constraint on it so that we always know we can
-- use 'show':
data Showable = forall a. (Show a, Typeable a) => Showable a
-- define show instance to just apply show to the contained type,
-- since we know that's always allowed:
instance Show Showable where
show (Showable a) = show a
-- now, we can put any arbitrary type into an array and show them all:
showables = [Showable (2 :: Int), Showable (3 :: Float), Showable "hello", Showable 'c']
-- show showables == [2,3.0,"hello",'c']
A neat trick here is that, if we have constrained the contained type to be Typeable
(as we did above), we can extract the original type back out of the Showable
container using Data.Typeable
's cast
method:
castShowable (Showable a) = cast a
Usage:
main> castShowable (Showable "hi") :: Maybe String
Just "hi"
main> castShowable (Showable "hi") :: Maybe Int
Nothing
We also need ExistentialQuantification
if we want to add class constraints to a data type, even if we don't want to hide the variable from the left hand side, for example:
data ShowableT a = Show a => ShowableT a
GADTs
Short for Generalized Algebraic Data Types, these allows you to declare new data
types using a function signature style. The advantage of this is that it allows you to decide exactly what the final type will be in each case, which is not possible with the current syntax.
A simple illustration of the difference:
-- take a simple type constructor:
data MyData a = Something a | Otherthing
Here, Otherthing
is of type MyData a
; There is no way to change that a
to something more specific. However, with GADT
syntax, we can. This is exactly equivalent to the above, but now we can specify what a
is if we like:
data MyData' a where
Something' :: a -> MyData' a
Otherthing' :: MyData' a
Why would we want to do this? One case is for when we want to use those parameters to encode extra information for us. Let's look at a larger example:
data Expr'' = S'' String
| I'' Int
| Add'' Expr'' Expr''
| Append'' Expr'' Expr''
A basic AST representation, but notice that Add
can take any Expr
even though the Expr
produced from I Int
is the only one that makes sense. We can use a type on Expr
to encode this, so that Add
only takes Expr Int
s, and Append
only takes Exp String
s:
data Expr' a = S' String
| I' Int
| Add' (Expr' Int) (Expr' Int)
| Append' (Expr' String) (Expr' String)
But without GADTs
we have no way to enforce that the type of, say, I Int
will actually be Expr Int
. With GADT's we express constructors as functions, and so we provide that final type:
data Expr a where
I :: Int -> Expr Int
Add :: Expr Int -> Expr Int -> Expr Int
S :: String -> Expr String
Append :: Expr String -> Expr String -> Expr String
Now, we can whip up a quick evaluator for our tree:
eval :: Expr a -> a
eval (S s) = s
eval (I i) = i
eval (Add a b) = eval a + eval b
eval (Append a b) = eval a ++ eval b
And notice that silly expressions are sanely rejected by the type system for us:
main> eval $ (I 1 `Add` I 2) `Add` (I 3)
6
main> eval $ (S "Hello" `Append` S " ") `Append` (S "World")
"Hello World"
main> eval $ (S "Hello" `Append` S " ") `Add` (S "World")
BZZZZT! Type Error. Add expects Expr Int, got Expr String's!
ImplicitParams
Allow functions to require that some value exists in the scope it's used in, without one having to actually explicitly pass in said value. Allows one to define global things in main
for instance and not have to think about threading them through everywhere explicitly, just adding an implicit requirement in functions where they are actually used. A simple example:
-- define implicit param as variable with question mark in front of:
fn1 a = fn2 a where ?world = " world"
fn2 a = fn3 a
fn3 a = fn4 a
-- add constraint that implicit param called ?world exists in the scope this
-- it used. Now, calling fn1 will work, but calling fn2, fn3 and fn4 directly
-- will fail as the implicit param would not be defined if not for fn1.
fn4 :: (?world :: String) => a -> String
fn4 a = a ++ ?world
Now, to test:
main> fn1 "hello"
"hello world"
main> fn2 2
BZZZT! ERROR! implicit param ?showIt not bound!
The downside? If you want to annotate your types, you have to explicitly thread your implicit parameters through in the type signatures, so why not just pass them normally? Calling something like tyfn1 "hello"
in this example would not work for instance:
tyfn1 :: String -> String
tyfn1 a = tyfn2 a where ?showIt = "world"
--this needs to mention implicit param type
--in order to work, so why not just pass it?
tyfn2 :: String -> String
tyfn2 a = tyfn3 a
tyfn3 :: (?showIt :: String) => String -> String
tyfn3 a = a ++ ?showIt
All in all, I think I'll steer clear of this one.
TypeFamilies
One of the most exciting language extensions I'm aware of so far, this one lets you teach Haskell's type system new tricks by defining relationships between types. This language extension actually introduces type families as well as data families, each with syntax do that you can use them inside classes. Let's start with type families themselves:
Type Families
This is a simple beginning example of an open type family. Think of type families as type aliases on steroids. This one maps the alias AddResult a b
to some corresponding type. It's called an open type family because we can add new instances to it later on if we want to extend it.
type family AddResult a b
type instance AddResult Double Double = Double
type instance AddResult Int Double = Double
type instance AddResult Double Int = Double
type instance AddResult Int Int = Int
If we were to use this, we'd probably end up wanting to define a type class, so let's do that:
class MyAdder a b where
adder :: a -> b -> AddResult a b
instance MyAdder Double Double where
adder a b = a + b
instance MyAdder Int Double where
adder a b = realToFrac a + b
instance MyAdder Double Int where
adder a b = a + realToFrac b
instance MyAdder Int Int where
adder a b = a + b
Here, we set the return type of this multi-parameter type class to be AddResult a b
, so that depending on what a
and b
are in the specific instances we can output completely different things. In this case we stick to Int
s and Double
s.
Naturally, we might feel that this type is associated to the type class, and want to actually state that part of the MyAdder a b
type class is the type AddResult a b
. Well, we can do exactly that:
class MyAdder' a b where
type AddResult' a b
adder' :: a -> b -> AddResult' a b
instance MyAdder' Double Double where
type AddResult' Double Double = Double
adder' a b = a + b
instance MyAdder' Int Double where
type AddResult' Int Double = Double
adder' a b = realToFrac a + b
instance MyAdder' Double Int where
type AddResult' Double Int = Double
adder' a b = a + realToFrac b
instance MyAdder' Int Int where
type AddResult' Int Int = Int
adder' a b = a + b
Now, AddResult a b
is an associated type within the MyAdder
type class, and so each instance will require a definition of it just as for any required functions. Functionally though it is the same as the original MyAdder
example.
Sometimes we might not want others to be able to arbitrarily extend a type family. Fortunately, GHC 7.8 introduced closed type families. Defining AddResult
again as a closed type family would look like:
type family AddResult'' a b where
AddResult'' Double Double = Double
AddResult'' Int Double = Double
AddResult'' Double Int = Double
AddResult'' Int Int = Int
One advantage of closed type families is that the evaluation order is now obvious; it works from top to bottom until your type is matched. This means that more general type aliases can come below more specific ones, which isn't possible in an open type family.
Type families can be recursive, too. This one takes some arbitrary function signature, and gives back its return type by recursing through it:
type family ReturnVal ty where
ReturnVal (a -> b) = ReturnVal b
ReturnVal b = b
Now, this would type check ('a' is a Char
):
'a' :: ReturnVal (Int -> String -> Char)
But this would not ('a' is not an Int
):
'a' :: ReturnVal (Char -> String -> Int)
In my next example I turn a nested 2-tuple into a function signature using another recursive type family. You may find you need to enable the extension UndecidableInstances
with certain recursive definitions when the type checker can't be sure that the recursion will ever end; the compiler will tell you if it is necessary and give this reason.
type family TupleFn ty out where
TupleFn () output = output
TupleFn (a,b) output = a -> (TupleFn b output)
We could use this definition to actually construct a function that takes a tuple, and a function generated from the tuple's type using the above (of whatever parity is required!), and applies the function to each value in the tuple, returning the result. We'll need another type class for this as we'll do something different depending on the shape (and thus type) of the tuple:
class ApplyFnToTuple a where
applyFnToTuple :: a -> TupleFn a out -> out
instance ApplyFnToTuple b => ApplyFnToTuple (a,b) where
applyFnToTuple (a,b) fn = applyFnToTuple b (fn a)
instance ApplyFnToTuple () where
applyFnToTuple _ fn = fn
The function signature asks for a tuple of type a
, and then some function of type TupleFn a out
, which is a type dictated by the shape of a
, and returns some out
, which is whatever the tuple function gives back. These would now both be true:
applyFnToTuple ('a',('b',())) $ \a b -> [a,b] == "ab"
applyFnToTuple ("hello",(12,('r',()))) $ \h n r -> h ++ show n ++ [r] == "hello12r"
We don't always need type classes to take advantage of type families in this way. If I create my own version of the above, I can avoid using them entirely:
-- we use these types to "tag" our list values. essentially,
-- they form a type level description that mirrors what we are
-- doing with values. We can improve on this with DataKinds, later.
data Cons a b
data Nil
-- the list itself, where the type 'a' is built from the above tags
data MyList a where
LCons :: itemty -> MyList a -> MyList (Cons itemty a)
LNil :: MyList Nil
-- this type family converts that type 'a' to a function signature.
type family MyListFn a output where
MyListFn (Cons a b) output = a -> (MyListFn b output)
MyListFn Nil output = output
-- this function applies items in `MyList a` to a `MyListFn a` just
-- like we did with tuples. Note no type family, because
-- no type dependant differences in behaviour needed:
applyFnToMyList :: MyList a -> MyListFn a out -> out
applyFnToMyList (LCons a b) fn = applyFnToMyList b (fn a)
applyFnToMyList LNil fn = fn
The crucial steps here are:
- We create a basic list type to use, here of type
MyList a
. - We want to record the type of each item we put into the list in the type
a
in some way that is easy to work with. Here we use a couple of new types,Cons a b
andNil
to represent at the type level what my list'sLCons
andLNil
constructors are doing at the value level. - Now that we have recorded the types in
a
, we use a type family to take the typea
and convert it into some desired typeb
, here a function signature. - Finally, we use this in
applyFnToMyList
, which takes in first aMyList a
(remember, thata
is the record of all types within), second a function of typeMyListFn a out
, and finally returnsout
. The definition of this function basically applies the function to each item in the list in the same order as the signature dictates.
We can use it just like the earlier tuple version:
applyFnToMyList (LCons 'a' (LCons 'b' LNil)) $ \a b -> [a,b] == "ab"
applyFnToMyList (LCons "hello" (LCons 12 (LCons 'r' LNil))) $ \h n r -> h ++ show n ++ [r] == "hello12r"
Awesome.
The Equality Constraint
Before looking at data families, another goodie that comes with the TypeFamily extension (and also with GADTs
) is the type equality constraint ~
. This basically allows us to tell the compiler that some type a
is equal to some other type b
. As an example, our earlier example avoiding FlexibleInstances
could be redefined from the original, which was:
class Truthy a where
truthy :: a -> Bool
class CharType a
instance CharType Char
instance CharType a => Truthy [a] where
truthy s = length s /= 0
To (now using an equality constraint):
class Truthy' a where
truthy' :: a -> Bool
instance a ~ Char => Truthy' [a] where
truthy' s = length s /= 0
Here, we define an instance for the general case of Truthy [a]
, but then add the constraint that a
must be a Char
. I expect this comes in particularly handy when you wish to define a function that operates on some type family but only when certain types match up (thus enabling the use of functions specific to those types). At times, it is useful just to provide the compiler with a little extra information if it is struggling to deduce some types.
Data Families
Also enabled with the TypeFamilies extension, these are the data
equivalent to type families as described above. Unlike type families where multiple type aliases can map to the same thing, each data instance is expected to have unique constructors; just like in any other data declaration we cant have multiple constructors with the same name.
An example I've seen before which I liked was that of storing different types in specialised key/value stores. Here, we define a new type MyMap key val
and then a constructor for each of MyMap Bool Val
and MyMap Int Val
. This shares the property of GADTs
in that we can set the type of key
and val
in MyMap key val
to whatever we like for each constructor:
data family MyMap key val
data instance MyMap Bool val = BoolMap (Maybe val) (Maybe val)
deriving Show
data instance MyMap Int val = IntMap [(Int,val)]
deriving Show
The equivalent in GADT form would be:
data MyMap key val where
BoolMap :: Maybe val -> Maybe val -> MyMap Bool val
IntMap :: [(Int,val)] -> MyMap Int val
Though you are unable to automatically derive Show
in the GADT version unlike for the data family declarations. The main advantage of data families however is that they are open and can be extended later on, for example if you create a new key
type you want a specialised MyMap
instance for.
We could create a type class and a couple of instances to put our MyMap
to use:
class MapKey k where
insert :: MyMap k v -> k -> v -> MyMap k v
find :: MyMap k v -> k -> Maybe v
remove :: MyMap k v -> k -> MyMap k v
instance MapKey Bool where
insert (BoolMap _ f) True v = BoolMap (Just v) f
insert (BoolMap t _) False v = BoolMap t (Just v)
find (BoolMap t _) True = t
find (BoolMap _ f) False = f
remove (BoolMap _ f) True = BoolMap Nothing f
remove (BoolMap t _) False = BoolMap t Nothing
instance MapKey Int where
insert (IntMap list) k v = IntMap ((k,v):L.filter (\(k1,v1) -> k /= k1) list)
find (IntMap list) k = case L.find (\(k1,v1) -> k == k1) list of
Nothing -> Nothing
Just (k,v) -> Just v
remove (IntMap list) k = IntMap (L.filter (\(k1,v1) -> k /= k1) list)
For our two map types, we define some basic operations to add, find and remove entries. If we add a new instance to our MyMap
data family at a later date for some new type, we can add a new instance to the associated type class for that same type and off we go. Just like type families, we can keep the data type that is clearly tied to this class inside it as an associated data type:
class MapKey' k where
data MyMap' k v
insert' :: MyMap k v -> k -> v -> MyMap k v
find' :: MyMap k v -> k -> Maybe v
remove' :: MyMap k v -> k -> MyMap k v
instance MapKey' Bool where
data MyMap' Bool val = BoolMap' (Maybe val) (Maybe val)
insert' (BoolMap _ f) True v = BoolMap (Just v) f
insert' (BoolMap t _) False v = BoolMap t (Just v)
find' (BoolMap t _) True = t
find' (BoolMap _ f) False = f
remove' (BoolMap _ f) True = BoolMap Nothing f
remove' (BoolMap t _) False = BoolMap t Nothing
-- ...other instances...
This works exactly as before, except now the requirement that you need to define a new data instance is imposed on adding new type class instances.
KindSignatures
This extension allows you to provide kind signatures to types, just as we would add type signatures to values. Basically in Haskell every type (I believe there are exceptions) has the kind *
. Just as types are categories of values (Int
is the category containing 0, 1, 2, 3.., Char
is the category containing 'a', 'b', 'c' and so on), kinds are categories of types (so the kind *
is the category containing type types Int
, Char
, Bool
and so on). A function type like Int -> Char -> Int
would have the kind * -> * -> *
. You could imagine categorising kinds into something too, and so on; some languages let you do this but Haskell currently stops at kinds (every kind is in the same category, and you can't change that).
It makes sense to use kind signatures when defining types and type families, as the variables you'd use instead are somewhat meaningless, for example in the data type:
data MyMap key val
What do key
and val
mean? Nothing, really. They are just place holders implying that the type MyMap
is actually of kind * -> * -> *
; it takes in two values (which can each be any normal Haskell type) and returns a normal Haskell type, itself. thus, we could write it instead as:
data MyMap :: * -> * -> *
Being explicit about the kind of MyMap
without using random variable names.
This becomes particularly useful with DataKinds and PolyKinds.
DataKinds
With this extension, we can actually create new kinds of our own that are separate from the existing kind *
. This is done by way of automatically promoting any suitable data type declarations one level.
Any basic types (not GADT's, and not using already promoted types for instance) are promoted to kinds, and their constructors promoted to types. Thus, if we write the simple data declaration:
data Number = One | Two
We get, in addition to our type Number
with constructors One
and Two
, the kind Number
and the types One
and Two
. But wait, wouldn't that potentially be ambiguous. Consider I also write:
data One
data Two
Now when I use the type One
somewhere, so I mean the latter type or the promoted type One
that belongs to our new Number
kind? The answer is the former. To resolve the ambiguity we prefix a promoted type with a single quote if we want instead to use that, so 'One
would be our promoted type.
How can we use this ability to create new kinds? Consider this definition:
data SomeData :: Number -> * where
OneS :: SomeData 'One
TwoS :: SomeData 'Two
Here, we say that our type SomeData
is of kind Number -> *
(using KindSignatures). This means that the first type it takes in must now be of kind Number
, so either 'One
or 'Two
(single quotes to disambiguate). Thus, the following would be an invalid definition:
a :: SomeData Int
a = undefined
Since Int
is of kind *
, rather than our new kind Number
. Remember our MyList
example from earlier, where we built up a type corresponding to the types of values we input. Let's rewrite that taking advantage of DataKinds:
-- use this as the new *kind* ListK with types ConsTy(..) and NilTy:
data ListK a = ConsTy a (ListK a) | NilTy
-- our MyList must use the above kind as its first argument:
data MyList' :: ListK * -> * where
LCons' :: itemty -> MyList' a -> MyList' (ConsTy itemty a)
LNil' :: MyList' NilTy
This all works exactly as it did before, but by doing this we've locked MyList
down so that the compiler will throw a hissy if we try using any invalid types like MyMap Int
or MyMap Bool
, whereas before the compiler would have let those types slip (and gone on to complain about the values of those nonsense types instead!)
TypeOperators
Type operators let us declare operators at the type level just like we would at the value level. For example, rather than our ListK
s ConsTy
type , we could use an operator. Here's a tweaked version of ListK
which does that:
data ListK2 a = a :+: ListK2 a | NilTy2
Now, we can use this new type operator in place of ConsTy
:
data MyList2 :: ListK2 * -> * where
LCons2 :: itemty -> MyList2 a -> MyList2 (itemty :+: a)
LNil2 :: MyList2 NilTy2
And our types look a little nicer as well:
LCons2 'a' (LCons2 'b' LNil2) :: MyList2 (Char ':+: (Char ':+: 'NilTy2))
The DataKinds extension also promoted Haskell's own list type to a kind, and so its operators and syntax become available at the type level (though we need this extension to use the now type-level list operator). Here's yet another implementation of MyList
, this time using Haskell's promoted list kind:
data MyList_ :: [*] -> * where
LCons_ :: itemty -> MyList_ a -> MyList_ (itemty ': a)
LNil_ :: MyList_ '[]
And our types get yet nicer:
LCons_ 'a' (LCons_ 12 LNil_) :: MyList2 '[Char,Int]
Sweet.
PolyKinds
Now we can introduce new kinds, we'll quickly find that we are restricted to working with one kind at a time, unlike types where we can define polymorphic functions to work across different types.
PolyKinds gives us this polymorphism at the kind level. Rather than explicitly naming the kind we'll be using (eg *
or Number
), we can use a variable eg k
just as we would for types in polymorphic functions.
Let's see why we'd want it with a type family example that reverses a promoted list:
type Reverse (a :: [*]) = DoReverse a '[]
--this type takes a type level array eg [Int,Char,String] and reverses
--it to eg [String,Char,Int].
type family DoReverse (a :: [*]) (b :: [*]) :: [*] where
DoReverse (a ': as) out = DoReverse as (a ': out)
DoReverse '[] out = out
Thanks to this type family, these two types would now be equivalent:
Reverse [Int,Char,String] ~ [String,Char,Int]
As we've done before when playing with Typefamilies, we could convert this list type into something more useful, for example a nested tuple:
type family TupFromList (a :: [*]) where
TupFromList (a ': as) = (a, TupFromList as)
TupFromList '[] = ()
So these would be now be perfectly valid ways to define tuples:
(12,('a',("hello",()))) :: TupFromList [Int,Char,String]
-- note that we can next type family applications just like functions:
("hello",('a',(12,()))) :: TupFromList (Reverse [Int,Char,String])
But I digress. What's PolyKinds got to do with all this? Well, this would fail to compile:
data LetterK = A | B
a :: Reverse [A,B]
a = undefined
Because we are trying to reverse an array of types of kind [LetterK]
, but our Reverse
type wants arrays of kind [*]
. Just like functions that take Int
s can't be passed Char
s, types expecting to be of the kind *
can't actually be of the kind LetterK
. With PolyKinds, we'd tell DoReverse
that it was of the kind [k]
, to denote that we don't care what concrete kind k
is, and any will do.
type Reverse_ (a :: [k]) = DoReverse_ a '[]
type family DoReverse_ (a :: [k]) (b :: [k]) :: [k] where
DoReverse_ (a ': as) out = DoReverse_ as (a ': out)
DoReverse_ '[] out = out
ConstraintKinds
This extension gives constraints, the things that ordinarily appear only to the left of =>
, their own kind, Constraint
. This allows them to be talked about like other kinds, expanding how we can make use of them somewhat. Let's have a quick look at a little of what we can do with this newfound power.
For a start, we can talk about them in type synonyms:
type ReadShow a = (Show a, Read a)
-- we can use these as we would the orignals:
readAndShow :: ReadShow a => a -> a
readAndShow val = read $ show val
This extends to associated types, which means we can (with Typefamilies
) use them in type classes to allow instances to alter the constraints used based on the input type. I found a use for this when I wanted to create a type class recently that did the following transformations:
Bool ==> a -> Bool
a -> Bool ==> a -> Bool
The typeclass to do this and its instances ended up looking like the following:
-- at this point, I dont know what the input to the function
-- will be, but I can make a constraint that in some way
-- connects it with the instance type:
class WithFunc ty where
type WithFuncC ty input :: Constraint
withFunc :: WithFuncC ty input => ty -> (input -> Bool)
-- given some function, I now know the input type,
-- and can thus connect it to the input type of the
-- returned function using the equality constraint (~):
instance WithFunc (a -> Bool) where
type WithFuncC (a -> Bool) input = a ~ input
withFunc = id
-- I don't care what type the input is, since
-- I know what i'll be outputting. () as a constraint
-- means that any type will be fine!
instance WithFunc Bool where
type WithFuncC Bool input = ()
withFunc out = \_ -> out
The Constraint
type is used here to restrict the input
type mentioned in the class definition in some way, depending on the instance types.
- The
(a -> Bool)
instance is able to say that the input to this provided functiona
will equal the input type of the returned function(input -> Bool)
mentioned in the class (since we'll just return it unchanged). - For the
Bool
instance, I can say that I don't care what the input type will be by effectively providing no constraint (since I have the output to the function, the boolean, already!).
Using this class, all of these are now perfectly valid:
withFunc True 1 == True
withFunc False 'd' == False
withFunc (== 'a') 'b' == False
withFunc (== 'a') 'a' == True
withFunc (< 7) 5 == True
Which is awesome.
Generally, it seems that ConstraintKinds
will play a useful role alongside TypeFamilies
for allowing the instances of some typeclass to dictate more of the details of their parent type classes definitions. Given some type mentioned in a class definition, TypeFamilies
allows it to be transformed depending on the type of each instance, whereas ConstraintKinds
can impose different per instance restrictions on it.
The End (for now)
That's all for now! I'll update this post as and when I feel the desire to summarize more language extensions, but for now thanks for stopping by! Let me know if you have any comments or suggestions below!