In order to see some more interesting examples of refinements, let's look at a data structure we're all familiar with: lists.
Let's look at some typical functions on lists and see how their type can be improved.
Built-in lists and the functions on these already have a lot of predefined features in Liquid Haskell. To better understand how to define datatypes in Liquid Haskell and establish properties on them, let us work with our own lists:
In principle, we can use these lists immediately in Liquid Haskell:
And we can even use them with refinement types:
However, if we try to define even a very simple recursive function over lists, we get an error:
Liquid Haskell tried to prove termination of all functions, but for user-defined datatypes, we have to teach it how to do this.
We can declare length
to be a measure by saying
and then declaring that length
is the termination measure to use for the List
type:
A measure is a single-argument function with one case per constructor. Measures are lifted into the types of the constructors of the function, and can then also be used in refinements.
Nil :: { ys : List a | length ys == 0 }
Cons :: a -> xs : List a -> { ys : List a | length ys == 1 + length xs }
With these declarations in place, length
now type- and termination-checks.
The following definition of map
now also termination-checks, because Liquid Haskell will assume that the function decreases the termination measure of its first "structured" argument:
Can you add a type signature that captures that map
does not change the length of the input list?
Termination is less obvious for interleave
:
In the recursive call, it is not clear that ys
is shorter than x : xs
.
In such a situation, we can specify a termination measure for a function ourselves:
Can you further refine the type signature such that we track the length of the resulting list?
Termination checking can be disabled for a whole file using the --no-termination
option.
Termination checking can be disabled for an individual function by using the lazy
directive.
Disabling termination checking can be a useful test, because quite often, termination errors are not easily recognisable as termination errors.
Liquid Haskell also checks totality, i.e., if a function covers all possible cases:
Can you provide a refinement type such that this function passes the totality check?
The definition of head
above has the disadvantage that it still triggers a warning in Haskell.
We can fix this by defining:
The precondition of impossible
is indeed impossible. So Liquid Haskell will verify that it cannot be called.
Fix this definition that now passes the totality check, but fails due to the Liquid Haskell type of impossible
:
Why does the following definition of take
not typecheck? Can you fix it?
Could you use impossible
in the definition of take
?
Also provide a proper refinement type for drop
:
And now for sublist
:
Let's test that this works:
Provide a suitable refinement type for replicate
:
With your refined type, this should typecheck:
And does this typecheck?
The function range
is supposed to take a lower and an upper bound and produce an ascending list of all the numbers that are greater than or equal to the lower bound and stricly smaller than the upper bound.
Try first to find a termination measure for this function so that it termination-checks.
Then try if you can refine the type further.
Implement a safe list indexing operation.
This is a simplified version of span
:
Can you find a better type?
But of course, we also want to capture that each of the groups is non-empty, and made up of equal elements (hint: you can use head
in refinements).
Ideally, the call to head
in the definition of encode
should be safe now:
Can you nevertheless refine the type signature further?