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?
Yes, but only when the input is a natural number. This would also require the result of fib
to be a natural number. Thus, to ensure termination you need the following specification of fib
:
{-@ fib :: i:Nat -> Nat @-}
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 “size” 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?
Yes, because the difference hi - lo
is decreasing in recursive calls. Thus, to ensure termination you need the following specification of range
:
{-@ range :: lo:Int -> hi:Int -> [Int] / [hi - lo] @-}
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?
Yes, because either m
is decreasing, or m
is the same and n
is decreasing. This is a lexicographic termination metric and can be encoded as [m,n]
. But, we need to ensure that m
and n
are non-negative, which in turn requires the result of ack
to be non-negative. Thus, to ensure termination you need the following specification of ack
:
{-@ ack :: m:Nat -> n:Nat -> Nat / [m, n] @-}
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.
The gcd of two numbers (which is not both zero) is the largest positive integer that divides both numbers. For example, the gcd of 8 and 12 is 4.
The Euclidean algorithm for computing the gcd of two numbers is based on the principle that the greatest common divisor of two numbers does not change if the larger number is replaced by its difference with the smaller number:
For example, gcd 8 12 = gcd 8 4 = gcd 4 4 = 4
.
gcd
function.
The metric is [a,b]
. Either a
is decreasing or it remains the same and b
is decreasing. Thus, to ensure termination you need the following specification of gcd
:
{-@ gcd :: a:Nat -> b:Nat -> Nat / [a, b]@-}
An alternative definition of gcd
is using the modulo operator. Instead of directly use the difference of the two numbers, ghc a b
is using the mod
to remove from a
as many b
s as possible:
For example, gcdMod 12 8 = gcdMod 8 4 = gcdMod 4 0 = 4
, because mod 12 8 = 4
and mod 8 4 = 0
.
Interestingly, termination does not require any explicit metrics, but follows from the semantics of the functions. That means, that if you properly refine the functions, termination will be guaranteed.
Question: Refine properly thegcd
and mod
functions to ensure termination.
Termination is ensured by the following specifications:
{-@ gcdMod :: a:Nat -> b:{Nat | b < a} -> Nat @-}
{-@ mod :: a:Nat -> b:{Nat | b /= 0} -> {v:Nat | v < b} @-}
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, that is usually part of merge sort algorithm.
Question: Let’s prove merge
terminating using a termination metric.
Termination is ensured by the following specifications:
{-@ merge :: xs:[a] -> ys:[a] -> [a] / [len xs + len ys]@-}
Question: Let’s also show that merge
propagates sortedness, by refining the inputs and output to be IList:
Sortedness is ensured by the following specifications:
{-@ merge :: xs:IList a -> ys:IList a -> IList a / [len xs + len ys]@-}
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.
Like with the Haskell’s lists, to ensure termination we need to provide a termination metric. In this case, the termination metric is the size of the List
data type:
{-@ lmerge :: xs:List a -> ys:List a -> List a / [llen xs + llen ys] @-}
Two functions are mutually recursive if they call each other. In such cases, Liquid Haskell will not attempt to prove termination automatically. Instead, the user needs to provide termination metrics. For example, consider the isEven
and isOdd
functions below.
isEven
and isOdd
functions.
The termination metrics can be the following:
{-@ isEven :: n:Nat -> Bool / [n, 0]@-}
{-@ isOdd :: m:Nat -> Bool / [m, 1]@-}
Note that at the definition of isOdd m
, the recursive argument remains the same. Thus, something should decrease! We define the termination metric [m, 1]
for isOdd
to ensure that the second component is decreasing. In the definition of isEven n
, the recursive argument decreases, so the second component of the termination metric is irrelevant.
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?
The termination metrics can be the following:
{-@ eval :: e:BExpr -> Bool / [size e, 0] @-}
{-@ evalAnd :: e:BExpr -> Bool -> Bool / [size e, 1] @-}
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.