loop
What is a Refinement Type? In a nutshell,
Refinement Types = Types + Predicates
That is, refinement types allow us to decorate types with logical predicates, which you can think of as boolean-valued Haskell expressions, that constrain the set of values described by the type. This lets us specify sophisticated invariants of the underlying values.
Let us define some refinement types:1
The Value Variable v denotes the set of
valid inhabitants of each refinement type. Hence, Zero
describes the set of Int values that are equal to
0, that is, the singleton set containing just
0, and NonZero describes the set of
Int values that are not equal to 0,
that is, the set {1, -1, 2, -2, ...} and so on. 2
To use these types we can write:
If we try to say nonsensical things like:
LiquidHaskell will complain with an error message:
../liquidhaskell-tutorial/src/03-basic.lhs:72:3-6: Error: Liquid Type Mismatch
72 |   one' = 1 :: Int
       ^^^^
  Inferred type
    VV : {VV : Int | VV == (1 : int)}
  not a subtype of Required type
    VV : {VV : Int | VV == 0} The message says that the expression 1 :: Int has the
type
    {v:Int | v == 1}which is not (a subtype of) the required type
    {v:Int | v == 0} as 1 is not equal to 0.
What is this business of subtyping? Suppose we have some
more refinements of Int
What is the type of zero?
Zero of course, but also Nat:
 and also Even:
and also any other satisfactory refinement, such as 3
Zero is the most precise type for 0::Int,
as it is a subtype of Nat, Even and
Lt100. This is because the set of values defined by
Zero is a subset of the values defined by
Nat, Even and Lt100, as the
following logical implications are valid:
In Summary the key points about refinement types
are:
Let’s write some more interesting specifications.
Typing Dead Code We can wrap the usual
error function in a function die with the
type:
The interesting thing about die is that the input type
has the refinement false, meaning the function must only be
called with Strings that satisfy the predicate
false. This seems bizarre; isn’t it impossible to
satisfy false? Indeed! Thus, a program containing
die typechecks only when LiquidHaskell can prove
that die is never called. For example,
LiquidHaskell will accept
 by inferring that the branch condition is always False
and so die cannot be called. However, LiquidHaskell will
reject
 as the branch may (will!) be True and so
die can be called.
Let’s use die to write a safe division function
that only accepts non-zero denominators.
From the above, it is clear to us that div is
only called with non-zero divisors. However, LiquidHaskell reports an
error at the call to "die" because, what if
divide' is actually invoked with a 0
divisor?
We can specify that will not happen, with a pre-condition that says that the second argument is non-zero:
To Verify that divide never calls
die, LiquidHaskell infers that
"divide by zero" is not merely of type String,
but in fact has the the refined type {v:String | false}
in the context in which the call to die occurs.
LiquidHaskell arrives at this conclusion by using the fact that in the
first equation for divide the denominator is in
fact
    0 :: {v: Int | v == 0} which contradicts the pre-condition (i.e. input) type.
Thus, by contradiction, LiquidHaskell deduces that the first equation is
dead code and hence die will not be called at
run-time.
The above signature forces us to ensure that that when we
use divide, we only supply provably
NonZero arguments. Hence, these two uses of
divide are fine:
Exercise: (List Average): Consider the function
avg:
n ?Next, let’s see how we can use refinements to describe the outputs of a function. Consider the following simple absolute value function
We can use a refinement on the output type to specify that the function returns non-negative values
LiquidHaskell verifies that abs indeed enjoys
the above type by deducing that n is trivially non-negative
when 0 < n and that in the otherwise case,
the value 0 - n is indeed non-negative. 5
In the above example, we compute a value that is guaranteed
to be a Nat. Sometimes, we need to test if a value
satisfies some property, e.g., is NonZero. For example,
let’s write a command-line calculator:
which takes two numbers and divides them. The function
result checks if d is strictly positive (and
hence, non-zero), and does the division, or otherwise complains to the
user:
Finally, isPositive is a test that returns a
True if its input is strictly greater than 0
or False otherwise:
To verify the call to divide inside
result we need to tell LiquidHaskell that the division only
happens with a NonZero value d. However, the
non-zero-ness is established via the test that occurs inside
the guard isPositive d. Hence, we require a
post-condition that states that isPositive only
returns True when the argument is positive:
In the above signature, the output type (post-condition) states that
isPositive x returns True if and only if
x was in fact strictly greater than 0. In
other words, we can write post-conditions for plain-old
Bool-valued tests to establish that user-supplied
values satisfy some desirable property (here, Pos and hence
NonZero) in order to then safely perform some computation
on it.
Exercise: (Propositions): What happens if you
delete the type for isPositive ? Can you
change the type for isPositive (i.e. write some
other type) while preserving safety?
Exercise: (Assertions): Consider the following assert
function, and two use sites. Write a suitable refinement type signature
for lAssert so that lAssert and
yes are accepted but no is rejected.
Hint: You need a pre-condition that
lAssert is only called with True.
Let’s wrap up this introduction with a simple truncate
function that connects all the dots.
 The expression truncate i n evaluates to i
when the absolute value of i is less than the upper bound
max, and otherwise truncates the value at the
maximum n. LiquidHaskell verifies that the use of
divide is safe by inferring that:
max' < i' from the branch condition,0 <= i' from the abs post-condition,
and0 <= max' from the abs
post-condition.From the above, LiquidHaskell infers that i' /= 0. That
is, at the call site i' :: NonZero, thereby satisfying the
pre-condition for divide and verifying that the program has
no pesky divide-by-zero errors.
This concludes our quick introduction to Refinement Types and LiquidHaskell. Hopefully you have some sense of how to
You can read the type of Zero as:
“v is an Int such that v
equals 0” and NonZero as : “v is
an Int such that v does not equal
0”↩︎
We will use @-marked comments to write
refinement type annotations in the Haskell source file, making these
types, quite literally, machine-checked comments!↩︎
We use a different names zero',
zero'' etc. as (currently) LiquidHaskell supports at
most one refinement type for each top-level name.↩︎
Dually, a standard Haskell type has the trivial
refinement true. For example, Int is
equivalent to {v:Int|true}.↩︎
LiquidHaskell is able to automatically make these arithmetic deductions by using an SMT solver which has built-in decision procedures for arithmetic, to reason about the logical refinements.↩︎