In this tutorial we introduce you to the PartialTypeSignatures
extension
available in GHC starting from version 7.10. The goal is to teach you what you
can do with this extension and how it can improve your development workflow.
Introduction
Partial type signatures were introduced in GHC 7.10 to give the programmer fine-grained control over type annotations. Prior to GHC 7.10, the programmer essentially faced a binary choice when it came to writing a type signature: do I write a complete type signature, or none at all?
Let's say you're writing a simple program tracking some preferences of your co-workers. Currently, we're only interested in the editor they're using. Let's define some data types to model this information.
data CoWorker = CoWorker String
deriving (Eq, Ord)
data Editor = Emacs | Vim
deriving (Eq)
Now let's define a function to remember a co-worker's editor preference.
We know we need at least two arguments, the co-worker and his/her editor
preference. However, we're not sure yet about the rest of the type,
maybe we'll go with a simple association list, i.e.
[(CoWorker, Editor)]
or maybe with a map, i.e. Map CoWorker Editor
, we don't know yet. With the limited information we currently have, our function will probably look like this:
rememberEditor coWorker editor = undefined
As we're not sure about what the function will actually do, we don't know its return type yet, which prevents us from annotating its type. Wouldn't it be helpful to already annotate the types of the arguments?
Imagine we're using this function later on. Our memory is a bit foggy and we accidentally swap the argument order:
test = rememberEditor Emacs (CoWorker "John")
Unfortunately, GHC's type checker will happily accept this code! As
rememberEditor
has no type signature yet, and its body is missing, GHC
has no clue about the types of the arguments.
Even though we knew the types of the arguments, we couldn't pass on this information to GHC's type checker as we couldn't annotate this (pattern signatures could help, but you have to sprinkle them throughout your program).
This is exactly the problem partial type signatures solves, by giving the programmer fine-grained control over type annotations. Let's add a partial type signature to our example so GHC's type checker can help us find our type errors again.
rememberEditor :: CoWorker -> Editor -> _
rememberEditor coWorker editor = undefined
A partial type signature looks just like a regular type signature except
for the fact that it can contain wildcards (written as underscores),
which take the place of the unknown parts of your type signature. The
wildcard in the (partial) type signature above thus indicates that the
return type (which can be another function) of rememberEditor
is unknown.
Now GHC's type checker can use these type annotations to detect the swapped argument order:
Couldn't match expected type `CoWorker' with actual type `Editor'
In the first argument of `rememberEditor', namely `Emacs'
Couldn't match expected type `Editor' with actual type `CoWorker'
In the second argument of `rememberEditor', namely `(CoWorker "John")'
After giving it some thought, we decide to go for a Map
and implement rememberEditor
as follows.
import Data.Map (Map)
import qualified Data.Map as Map
rememberEditor :: CoWorker -> Editor -> _
rememberEditor coWorker editor = Map.insert coWorker editor
Now GHC's type checker will even inform us of the type to replace the wildcard with!
Found hole `_'
with type: Map CoWorker Editor -> Map CoWorker Editor
...
A partial type signature combines type checking (type annotations) with type inference (wildcards) in one type signature.
Syntax & Options
There are a number of different kinds of wildcards you can use, as well compiler flags to control the warnings and/or errors.
Type Wildcards
The simplest wildcard is the type wildcard. For instance the wildcard used in the example above was a type wildcard. A type wildcard is a wildcard that occurs in the type part of a type signature, or put in other words, a wildcard not occurring in the constraints of a type signature.
A wildcard can be filled in with a closed type like Int
, [String]
,
Editor
; a function like Int -> Bool
; a type containing a type
variable like a -> Bool
; a higher-kinded type like Maybe
, Either
,
Either a
, m
; pretty much every type.
When a wildcard is unconstrained, i.e. it is not filled in with any type after type inference, it is generalized over: it is replaced with a new type variable.
Take for example this standard filter
function:
filter :: (a -> Bool) -> [a] -> [a]
filter p xs = [x | x <- xs, p x]
The following partial type signatures are all valid. The type checker will each
time infer exactly the same type for filter
: (a -> Bool) -> [a] -> [a]
.
(a -> _) -> [a] -> [a]
(_ -> Bool) -> [a] -> [a]
({-hi-}_ _ _{-/hi-}) -> [a] -> [a]
_ -> [a] -> [a]
_ -> [a] -> [_]
_ -> [a] -> {-hi-}_{-/hi-} a
_ -> [a] -> _
_ -> _ -> _
_ -> {-hi-}_{-/hi-}
{-hi-}_{-/hi-}
Highlighted wildcards:
a -> Bool
can also be written in prefix-notation as(->) a Bool
. The first wildcard will be filled in with(->)
, the second witha
, and the third withBool
.The wildcard will unify with
[]
, the type constructor for lists.As a wildcard can unify with a function type, a single wildcard can be filled in with a function with any arity.
This partial type signature can be left out, as it is the same as writing no type signature at all and fully rely on type inference.
Named Wildcards
Wildcards can also be named, to clarify their meaning, but more
important, to refer to them later on. All wildcards with the same name
within one type signature will be filled in with the same type. You can
name a wildcard by writing an identifier after the underscore of the
wildcard, e.g. _foo
.
Haskell 2010 and previous versions of GHC interpret named wildcards like
_foo
as type variables. To remain backwards compatible, named wildcards are hidden behind theNamedWildCards
extension flag. Remember to add the following line to the top of your file when you want to_foo
to be interpreted as a named wildcard instead of a type variable.
{-# LANGUAGE NamedWildCards #-}
Let's say we wrote the following partial type signature for filter
:
-- Partial type signature with named wildcards
filter :: (_x -> _x) -> [_x] -> [_x]
-- Inferred type
filter :: (Bool -> Bool) -> [Bool] -> [Bool]
-- Most general type of filter
filter :: (a -> Bool) -> [a] -> [a]
The second occurrence of _x
is filled in with Bool
after type
inference, all other instances of _x
were not filled in with any type.
If they hadn't been named wildcards, they would have been replaced with
a type variable, e.g. a
. But because they have the same name as the
wildcard filled in with Bool
, they are all filled in with Bool
.
When a named wildcard is not filled in with any type after type
inference, it is generalised over, just like an unnamed wildcard. There
is one small difference: the new type variable will have w_NAME
as
name where NAME
is the name of the named wildcard.
identity :: _foo -> _foo
identity x = x
-- Inferred type:
identity :: w_foo -> w_foo
Named wildcards must not be confused with type variables because they
can still be filled in with a closed type like Int
, e.g.
not :: _foo -> _foo
not True = False
not False = True
-- Inferred type:
not :: Bool -> Bool
Constraint Wildcards
As you might have guessed, wildcards occurring in the constraints part are called constraint wildcards. Unfortunately, constraint wildcards aren't all that useful. Take the following example.
showPlusOne :: _ a => a -> String
showPlusOne x = show (1 + x)
Because we use show
and (+)
, x
must implement both the Show
and
the Num
type classes. Should the wildcard be filled in with Show
or
Num
? We certainly don't want the type checker to guess! Unlike types,
which are unified to fill in the wildcards, constraints are solved,
which isn't amenable to filling in wildcards. Therefore, wildcards in
constraints are not supported.
However, some wildcards do make sense in constraints, but only when they comply with certain restrictions to keep the type checker from guessing. The rules are as follows.
- Only named wildcards are allowed in constraints ...
- ... when they're also present in the rest of the type
Of the following type signatures only the last one satisfies both rules and will be allowed.
Eq _ => a -> a -> Bool -- No
Eq _x => a -> a -> Bool -- No
Eq _x => _x -> _x -> Bool -- Yes
When these rules are followed, regular type inference will take care of filling in the wildcards, and the type checker won't have to guess.
Extra-constraints Wildcard
There is one more kind of wildcard, the extra-constraints wildcard, not to be confused with constraint wildcards. Whereas constraint wildcards occur within a constraint, an extra-constraints wildcard occurs as a constraint. When a partial type signature contains an extra-constraints wildcard, it means that the type checker may infer any number (0..n) of extra constraints, which will then be integrated in the final type of the binding. For example:
showPlusOne :: _ => a -> String -- Inferred for `_': (Num a, Show a)
showPlusOne x = show (1 + x)
The type checker will infer the extra constraints Num a
and Show a
,
just as when you would have left out the type signature. The two
constraints will be used to fill in the extra-constraints wildcard.
You can combine annotated constraints with an extra-constraints wildcard. There are however some restrictions: there can't be more than one extra-constraints wildcard in a type signature and it must come as the last constraint.
Some more examples:
showNum :: (Num a, _) => a -> String
showNum x = show x
-- Found hole `_' with inferred constraints: (Show a)
-- ..
forM :: _ => _
forM x f = mapM f x -- mapM :: Monad m => (a -> m b) -> t a -> m (t b)
-- Found hole `_' with inferred constraints: (Monad m, Traversable t)
-- ..
--
-- Found hole `_' with type: t -> a -> (a -> m b) -> m (t b)
-- ..
false :: _ => Bool
false = False
-- Found hole `_' with inferred constraints: ()
-- ..
In the filter
example, we said that the partial type signature
filter :: _
is the same as writing no type signature at all. This
wasn't entirely true, because extra constraints are not allowed without
the extra-constraints wildcard. But now we have all the ingredients to
define the most general partial type signature: _ => _
, which you can
leave out just as well, as it is equivalent to writing no type signature
at all!
Flags
In past version of GHC, wildcards (underscores) in type signatures produced parse errors. Starting from GHC 7.10 they no longer produce parse errors but type errors.
Take the following simple program:
x :: _
x = True
Compiling this with GHC 7.8 produces:
Tutorial.hs:1:6: parse error on input `_'
Whereas compiling it with GHC 7.10 produces:
Tutorial.hs:1:6:
Found hole `_' with type: Bool
To use the inferred type, enable PartialTypeSignatures
In the type signature for `x': _
The error message now informs you of the inferred type of the wildcard
or hole, just like TypedHoles
tells you the type of a hole. However,
unlike TypedHoles
which requires you to choose the right expression of
that type, knowing the type of a wildcard or hole is enough to fill in
the hole!
When you want to compile your program without manually filling in the holes,
just enable the PartialTypeSignatures
extension flag, which will demote the
errors caused by holes in type signatures to warnings and use the inferred
types to fill in the holes. When you don't want these warnings either, pass
-fno-warn-partial-type-signatures
to GHC and it will suppress the warnings
too.
Remember from the section on Named Wildcards that you need the NamedWildCards
extension flag to use named wildcards, otherwise they will be interpreted as
type variables.
Use Cases
Partial type signatures can come in handy in the following use cases:
During development, like in the introductory example. By annotating the types you already know, even though you don't know the whole type yet, you give the type checker information to help catch your type errors. Furthermore, a (partial) type signature is a form of machine-checked documentation that you can quickly glance at to remind you of the type of the function, or at least the parts you already know.
Interactive hole-driven development in combination with
TypedHoles
. The type checker just tells you what types to fill in.The type signature is too verbose and complicated. By replacing the verbose or complicated parts with underscores you can direct the user's focus to the parts of the type signature that really matter. Or when a bunch of related functions have similar but verbose types, the distracting common boilerplate can be hidden with underscores, thereby stressing the differences.
Not all programs can be written without type annotations as some types cannot be inferred, e.g. higher-rank types. With a partial type signature, you can annotate the parts required for type inference to succeed, but leave out the boilerplate. In the example below, the argument of
foo
cannot be inferred and needs a type annotation. However, the return type can easily be inferred, but why should we have to annotate it as well?foo :: (forall a. [a] -> [a]) -> {-hi-}([Bool], [Char]){-/hi-} foo x = (x [True, False], x ['a', 'b']) test = foo reverse -- reverse :: forall a. [a] -> [a]
With a partial type signature:
foo :: (forall a. [a] -> [a]) -> {-hi-}_{-/hi-} foo x = (x [True, False], x ['a', 'b']) test = foo reverse -- reverse :: forall a. [a] -> [a]
Further reading: