sum
and psum
mRTheorem
mRTheorem
on plusBy default Liquid Haskell checks that every function terminates. This is required for two reasons:
Up to now, we were deactivating the termination checker with the --no-termination
flag or with the lazy
annotation. Now, we will see how to prove termination in cases where Liquid Haskell cannot do it automatically.
We start with the known fibonacci function.
Liquid Haskell will create an error in the above definition, even though it does not violate any refinement type specification.
This is a termination error and it will disappear if we turn off termination checking (either with --no-termination
pragma or with {-@ lazy fib@-}
annotation).
Question: Does fib
terminate?
To ensure fib
terminates we need to restrict the input to be non-negative. This is actually implied by the error message that requires the recursive argument to be 0 <= v
and v < i
.
This error was generated because Liquid Haskell was trying to prove termination of the function fib
by applying its termination heuristic.
Termination Heuristic: The first argument that can be “sized”, i.e., turned into nat, should be decreasing in recursive calls and non negative.
Int
is by default “sized” while later we will see how to make other types “sized”.
The heuristic fails in many cases. For example, consider the function range
that generates a list of integers from lo
to hi
.
Question: Does range
terminate?
The termination heuristic fails because the first argument is not decreasing in recursive calls. To specify that the value hi - lo
is decreasing we need to introduce a termination metric:
{-@ range :: lo:Int -> hi:Int -> [Int] / [hi - lo]@-}
Termination metrics are integer expressions that can depend on the function arguments and once provided, Liquid Haskell will use them to check termination. Concretely, at each recursive call it will check that the termination metric is both decreasing and non-negative.
Many times a single natural number is not enough to specify termination. For example, consider the ackermann function:
Question: Does ack
terminate?
To show that ack
terminates we need to provide a lexicographic termination metric. Now at each recursive call, Liquid Haskell will check that the first component of the metric is decreasing and if it is equal, it will check the second component, etc.
The Greater Common Divisor (gcd) function is an interesting example, because it might or not require lexicographic termination.
Question: Refine properly the gcd
and mod
functions to ensure termination.
Question: Provide the proper lexicographic termination metric for the below gcd
function.
When recursive functions are defined on data types, Liquid Haskell will first look for structural termination, meaning that the recursive calls are on a structural subpart of the input. For example, the map
definition below terminates because xs
is a subpart of x:xs
.
Of course, not all recursive functions on data types are structurally terminating. As an example consider the merge
function below.
Question: Let’s prove merge
terminating using a termination metric.
Question: Let’s also show that merge
propagates sortedness, by refining the inputs and output to be IList:
In user defined data types, Liquid Haskell tries to prove structural termination. For example, mapping over a list defined as a user defined data type will not require a termination metric.
The user can provide a size for each user defined data type. Here, for example, we define the size of a List
to be the length of the list.
Now, when structural termination fails, Liquid Haskell will use the size of the data type to check termination. For example, note the termination error provided in the lmerge
function below.
On mutual recursion, the user needs to provide termination metrics. For example, consider the isEven
and isOdd
functions below.
Question: Provide the proper termination metrics for the isEven
and isOdd
functions.
This pattern of providing numeric values for lexicographic termination metrics appears very often in mutually recursive functions. For example, the below code is a simplification of a real world example and follows the same pattern.
Question: Provide the proper termination metrics for the eval
and evalAnd
functions?
Liquid Haskell, by default, checks that every function terminates. It has three mechanisms to prove termination:
The --no-termination
flag or the {-@ lazy @-}
annotation can be used to deactivate the termination checker, either because the user is not willing to prove termination or because the functions are intentionally non-terminating.