Welcome to CPSC 312!
import System.Random
import Data.List
1 Quick Note on Health and Masks
Masks are mandated in class. I’m immune suppressed. For me and others at-risk or close to those who are at-risk, please wear your mask, including over your nose.
However, there are medical circumstances that merit exemptions. To seek such accommodation, please contact the Centre for Accessibility at info.accessibility@ubc.ca.
While we’re on the subject: please get vaccinated if you can (it’s another line of defence for those around you who cannot!) and please do not come to class if you’re sick (there are so many good online options for us!).
2 Showing Off in Haskell
Haskell is a pure, lazy, statically-typed functional programming language with powerful type inference. You’ll learn about a lot of that in the reading. Today, we’re just going to show off with a bit of what that means.
For now, we need to know that: a Haskell program is just an expression to be evaluated, not a series of statements to execute. Programs build up complex expressions to be evaluated by giving names to and using smaller expressions (functions and variables).
2.1 Quicksort in English
Quicksort is a beautiful sorting algorithm. Here’s a simple version. To quick sort a list:
- If the list is empty, simply return it.
- Otherwise, let
p
be the first element of the list. Construct two sublists: everything else that is< p
and everything else that is>= p
. Quick sort each sublist recursively. Then return the quick-sorted< p
list, followed by a list containing justp
, followed by the quick-sorted>= p
list.
Now, we could read all that. Or, we could sing it instead.
2.2 Quicksort in Haskell
First, let’s write Quicksort in Haskell. The first line is the signature. The second is the function definition… which is undefined so far. We’ll finish it!
qsort :: Ord a => [a] -> [a]
= undefined qsort _
What did we see? Haskell:
- Uses (beautiful?) pattern-matching to make decisions.
- Has polymorphic types. In this case, ad hoc polymorphism:
qsort
sorts lists of any type that is an instance ofOrd
(like a Java interface). - Supports list comprehensions1 that can be quite elegantly mathematical.
2.3 Test Data for Quicksort
Let’s test qsort
a bit: on []
, on [2, 4, 6, 0, 1]
, and on "mindblowing"
. We’ll use ghci
for its REPL (read-eval-print-loop) to try these out. (Exercise!)
Next, let’s try qsort
on something bigger. We’ll need some input to call it on:
-- | @genList cap seed@ returns an unending list of (pseudo-)random
-- @Int@s in the range @[0, cap)@, using @seed@ for the random
-- generator. (Don't worry about the implementation for now!)
genList :: Int -> Int -> [Int]
= map (`mod` cap) (randoms (mkStdGen seed)) genList cap seed
We could try out qsort
with:
infResults :: [Int]
= qsort (genList 10000 0) infResults
But wait. How many elements are we sorting?! Try infResults
at the ghci
REPL. (Wait… why didn’t that run forever before we typed infResults
?)
(Exercise!)
2.4 Finite Test Data
Here’s code for a random list of n
numbers in the range [0, n)
, plus a sample bigList
to work with.
randList :: Int -> [Int]
= take n (genList n 0)
randList n
bigList :: [Int]
= randList 1000000 bigList
The take
function uses parametric polymorphism. take n lst
produces the first n
elements of the list lst
, without caring what type of value is in lst
. Its type is Int -> [a] -> [a]
: given an Int
and a list of any type of value a
, return a list of (that same type of) a
values.
You can run bigList
at the REPL, but it prints for a long time. Instead, get just the last element of bigList
:
:set +s -- turn on performance information
last bigList
How long did that take? Try last bigList
again. How long did it take the second time? (Exercise!)
Haskell uses lazy evaluation. Loosely: it evaluates expressions only when forced to, e.g., by you the user. (Once it evaluates a variable’s expression, it caches it to avoid evaluating it again.)2
2.5 Laziness Goes Deep
Let sort bigList
but just look at the last result. Run last (qsort bigList)
.
How long did that take?
How about take 100 (qsort bigList)
? (That’s the first 100 elements of the sorted list.) How long did that take? Why?!?
(Exercise!)
3 Showing off in Prolog
As strange as Haskell is, Prolog is stranger. That makes it a little easier to show off.
Above, we relied on Haskell’s ++
operator to append two lists. It’s defined as:
(++) :: [a] -> [a] -> [a]
++ ys = ys
[] :xs) ++ ys = x : xs ++ ys (x
Let’s redefine this in Prolog. We’ll call it append
.
3.1 Functions That Are (Not) Functions
Haskell’s “functions” aren’t like Java “functions” because Java “functions” are not functions. You can call a Java function twice with the same arguments and (because it sets variables, reads input from the user, accesses a datasource via REST, etc.), it can return two different things or move a robot or something whacky like that.
That is not how mathematical functions work. So, Haskell does many clever things to use real mathematical functions while still being able to read input from the user and use REST APIs and such.
Prolog really doesn’t do normal “functions”.
append
doesn’t return the result of appending two lists. Instead, it is a (logical) predicate of three values that describes the relationship in which the third value is the result of appending the first two.
The Haskell code above kind of tells us our cases. append(Xs, Ys, Zs)
is true when:
Xs
is just the empty list andYs = Zs
.Xs
andZs
start with the same element (one element down!), andappend
is true for the rest ofXs
,Ys
, and the rest ofZs
.
3.2 Append in Prolog
Let’s write that together in Prolog.
Notes: append(X, Y, Z) :- stuff(over, here)
means “if stuff(over, here)
is true, then append(X, Y, Z)
is true”. Also, anything that starts with an uppercase letter is a variable; anything else is just an atom (like a symbol or a bit like a string).
% Fill in the blanks on append/3 so that append(Xs, Ys, Zs)
% is true if appending together Xs and Ys results in Zs.
, , ) :- .
append( , , ) :- . append(
(Exercise!)
3.3 What a Predicate Can Do, Forwards and Backwards
Let’s try that in the swipl
(SWI-Prolog) REPL.
It’s downright strange what we can do with append
now that we’ve defined it. Remember that append(Xs, Ys, Zs)
just describes the circumstances under which it is true that Zs
is the result of appending Xs
and Ys
.
So, start by trying append([1, 2], [3, 4, 5], Zs).
Then, start asking for stranger things.
(Exercise!)
We’ll try to show off two interesting features of Prolog:
- Prolog determines variables’ values using “unification”. (In fact, it can use unification to check if any two expressions can be made equal to each other and keep track of the constraints needed to make that so.)
- A Prolog “program” is a description of the world plus a query. Prolog “runs the program” by searching for conditions that can make that query true and giving them to you. (You can even ask Prolog to keep searching after it finds the first set of conditions that work.)
4 Class Notes for Today
- Let’s spend a few minutes with the Syllabus. (Especially important: do not come to class if you are sick!!)
- In-class Exercises 1 is on PrairieLearn and due Thu Sep 16. If you get something wrong, just try it again. Use these exercises to learn in class and prepare for the quiz!
- Quiz 1 is coming up on Friday September 17 (during class time).
- Assignment 1 is due Sep 23 on PrairieLearn. Submit by Sep 16 for a chance at some extra credit.
- To reduce Covid transmission risk, please sit in the same area each lecture when possible.
- Before next class:
- access PrairieLearn and Piazza from the Resources page
- get Haskell set up so you can try things out; the Resources page has help
- work through CIS 194 Lecture 1 up to the “GHCi” section
5 Appendix
Monday Morning Haskell recently released a In-Place QuickSort in Haskell video. If you haven’t already studied Haskell, the latter 2/3 of the video is (very) challenging. If you want to give it a try, you can think of a “monad” for this context as a special structure for assembling computations-that-produce-values-when-run that ensures you don’t accidentally mistake those computations with the values they produce.
Here are some longer/different versions of the code above.
-- | @qsort xs@ should produce a sorted version of xs.
--
-- Haskell's doctest can turn these @>>>@ examples into
-- runnable tests. It actually also has a powerful testing
-- system where you assert logical expressions as properties.
--
-- >>> qsort []
-- []
--
-- >>> qsort [2, 4, 6, 0, 1]
-- [0,1,2,4,6]
--
-- >>> qsort "mindblowing"
-- "bdgiilmnnow"
qsort :: Ord a => [a] -> [a]
= []
qsort [] :ps) =
qsort (p| s <- ps, s < p]
qsort [s ++ [p]
++ qsort [b | b <- ps, b >= p]
-- | @genList cap seed@ takes a seed value and returns an unending
-- list of (pseudo-)random Int values in the range [0, cap).
--
-- Here's a more idiomatic version that "chains" together existing
-- functions using the function composition operator @.@.
genList :: Int -> Int -> [Int]
= map (`mod` cap) . randoms . mkStdGen genList cap
List comprehensions actually demonstrate some of Haskell’s fascinating type classes that support common and flexible ways to build large computations out of smaller ones.↩︎
ghc
did not cachelast bigList
because we didn’t attach that to a name. It did cache thebigList
because in demanding the value oflast bigList
, we also demanded that it evaluatebigList
all the way to its last element.)↩︎