The next three weeks of lecture will be less focused on particular tools. Some questions we’ll be considering: Beyond just modeling tools, where would you like to see formal methods applied? What does it mean to prove something? How can logic inform programming languages? Have you (possibly unknowingly) used formal methods in programming before?
What are some programming languages that you have used before?
How have these languages helped you “reason about your code”?
When do these happen?
Type checking can be thought of as a formal methods tool! Theorists have snuck their math/logic into most programming languages.
Another language we haven’t mentioned - Haskell! We will be using a variant of Haskell next in this course.
A warning: there can be a tribal division in Computer Science about the “best/most-beautiful/most-elegant” languages. We’re going to avoid that entirely!
Haskell has many interesting features. It’s:
Let’s write our first Haskell program!
Haskell has type inference, so we don’t always need to give expressions types explicitly. However, this can lead to unintentional ambiguity, so for now we’ll be writing all of our types.
divide :: Int -> Int -> Int
divide n d = div n d
Syntax notes:
::
can be read as “is type”->
is used to denote a function, but can also be thought of as we’ve seen it previously, as a cross product.div
is Haskell’s build in division operatorLet’s run our program:
$ divide 10 2
2
What can we give this program to break it, even though it already type-checks?
$ divide 5 0
*** Exception: division by zero
The type alone doesn’t allow us to known if the second term is zero.
Let’s add a piecewise component to our function to handle this:
divide :: Int -> Int -> Int
divide n 0 = error "this is a custom exception"
divide n d = div n d
Lists in languages like Haskell are less like arrays of values, and more like linked lists, defined recursively. The base case is the empty list, and longer lists are single elements concatenated onto other lists! The list [1, 2, 3] is 1, concatenated onto (2, concatenated onto (3, concatenated onto empty)).
Let’s try to write a function that doubles everything:
double_all :: [Int] -> [Int]
double_all [] = []
double_all (x:xs) = 2*x : (double_all xs)
double :: Int -> Int
double x = 2*x
double_all_map :: [Int] -> [Int]
double_all_map lst = map double lst
mult_all_by_k :: Int -> [Int] -> [Int]
mult_all_by_k k lst = map (\x -> (k*x)) lst
average :: [Int] -> Int
average lst = divide total n
where
total = sum lst
n = length lst
When we run our average function:
$ average [1, 2, 3]
2
$ average []
this is a custom exception
Let’s try a sorting algorithm: we’ll start with insertion sort.
isort :: [Int] -> [Int]
isort [] = []
isort (x:xs) = insert x (isort xs)
insert :: Int -> [Int] -> [Int]
insert x [] = [x]
insert y (x:xs) =
| y < x = y:(x:xs)
| otherwise = x:(insert y xs)
Now wait - we’ve gone through this entire class, and the best we can say about the output of this function is that it’s a list of integers?
The second lecture, we talked about the properties we want sorted lists to have. What were they?
Does our type say anything about this? No! We’d like our types to express this! Haskell on its own doesn’t give us this, but next lecture we’ll use Liquid Haskell, which does!
More syntax notes:
\
is a lamda and is used to define anonymous functions (ones the programmer doesn’t need/want to name):
is concatinating an element onto a list(x:xs)
is pattern-matching on a list: we’re in a sense “unwrapping” the list by peeling off the first elements, naming it x
, and naming the rest of the list xs
|
is used to break an expression into cases, and otherwise
is like an else-block