Refinement types are an approach for expressing more detailed properties of data types using propositional logic predicates. Liquid Haskell is a static refinement type checker capable of veryfing refinement type safety. We will examine the possibilites this approach opens for increasing development productivity.
5. Standard type system
●
Allows expressing certain properties of programs
●
Type safety
●
Verifiable without running the program
●
Static type checking
●
Integrated with the compilation
●
Testing still needed
●
Can we do better ?
6. Possible improvements
●
Prevent more programming errors
●
Division by zero
●
Missing keys in maps
●
Infinite loops
●
Express properties of programs in greater detail
●
Keep the ability to automatically verify type safety
●
Verification must be a decidable problem
●
No proofs by the programmer required
8. Refinement types
●
Consist of
●
Type
●
Standard or refinement
●
Predicate
●
Propositional logic
●
Can describe valid inputs and outputs of functions
●
Type safe if the predicate is valid for all inputs
11. Liquid Haskell
●
Static refinement type verifier
●
Completely automatic
●
Translates refinement types into verification conditions
●
Satisfiability modulo theories formulas
●
Uses an SMT solver to verify those conditions
●
Without executing the program or enumerating inputs
●
Project at University of California - San Diego
●
http://goto.ucsd.edu/~rjhala/liquid/haskell/blog/about/
12. Defining refinement types
●
Positive is a subtype of NonZero
●
Positive values are a subset of NonZero values
{-@ type NonZero = {v: Int | v /= 0 } @-}
{-@ type Positive = {v: Int | v > 0 } @-}
{-@ type Odd = {v: Int | v mod 2 == 1 } @-}
{-@ one :: NonZero @-}
{-@ one :: Positive @-}
{-@ one :: Odd @-}
one :: Int
one = 1
{-@ odds :: [Odd] @-}
odds :: [Int]
odds = [1, 3, 7]
13. Refining function results
{-@ two :: {v: Int | v mod 2 == 0 } @-}
{-@ one, two :: NonZero @-}
two :: Int
two = 1 + 1
{-@ size :: [a] -> {v: Int | v >= 0 } @-}
size :: [a] -> Int
size [] = 0
size (x:xs) = 1 + size xs
{-@ positive :: n:Int -> { v: Bool | Prop v <=> n > 0 } @-}
positive :: Int -> Bool
positive n = n > 0
14. Refining function arguments
{-@ crash :: {v: String | false } -> a @-}
crash :: String -> a
crash message = error message
{-@ divide :: Int -> NonZero -> Int @-}
divide :: Int -> Int -> Int
divide n 0 = crash "division by zero"
divide n d = n `div` d
correctDivide :: Int
correctDivide = divide 1 1
incorrectDivide :: Int
incorrectDivide = divide 1 0
15. Defining predicates
{-@ predicate Positive N = N > 0 @-}
{-@ predicate Even N = N mod 2 == 0 @-}
{-@ predicate PositiveOdd N = Positive N && not Even N @-}
{-@ type Even = { v: Int | Even v } @-}
{-@ three :: { v: Int | PositiveOdd v || v == 4 } @-}
three :: Int
three = 5 - 2
16. Measure functions
●
Can be used inside refinement type definitions
●
Single expression for every data constructor
●
Propositional logic only
data List a = Emp
| (:::) a (List a)
{-@ measure len @-}
len :: List a -> Int
len Emp = 0
len (x:::xs) = 1 + len xs
{-@ first :: {v: List a | len v > 0 } -> a @-}
first Emp = crash "empty list"
first (x:::xs) = x
17. Refining data types
●
Parametrized type alias used to specify list length
data Triple a = Triple (List a)
{-@ type ListN a N = {v: List a | len v == N} @-}
{-@ data Triple a = Triple (ListN a 3) @-}
correctTriple = Triple (1 ::: (2 ::: (3 ::: Emp)))
18. Inline functions and assumptions
●
Inline functions can be used inside measures
●
Assumptions allow describing non-verifiable functions
{-@ inline increment2 @-}
increment2 :: Int -> Int
increment2 n = n + 2
{-@ measure doubleLen @-}
doubleLen :: List a -> Int
doubleLen Emp = 0
doubleLen (x:::xs) = increment2 (doubleLen xs)
{-@ assume abs :: (Num a) => a -> {v: a | v > 0 } @-}
19. Recursion
{-@ type NonNegative a = {v: a | v >= 0 } @-}
{-@ type Natural a = {v: a | v > 0 } @-}
{-@ fact :: (Integral a) => NonNegative a -> Natural a @-}
fact :: (Integral a) => a -> a
fact 0 = 1
fact n = n * fact (n – 1)
correctFact = fact 3
incorrectFact = fact (-1)