Welcome to CPSC 312!
Complete the associated in-class exercises.
Note: If you are running in a PL workspace with this lecture loaded, you should be able to run cabal repl
to load it and interact with it in the REPL. If you are running it stand-alone, try cabal repl --build-depends "random"
and then :load FILENAME
(replacing FILENAME
with the name of the lecture file).
1 Today’s Notes 2024/09/04
- Let’s spend just a few minutes with the website, especially the Syllabus.
- Office hours start Monday; see Piazza
- In-class Exercises 1 is on PrairieLearn and due Mon Sep 16. If you get something wrong, just try it again. Use these exercises to learn in class and prepare for the quiz!
- Use it to follow along today and every day!!
- Quiz 1: Friday September 20 (during class time)
- Assignment 1 will release soon.
- 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
Before we get started, let’s get Agora set up. Code: 53e8a93f
module Lib where
import System.Random
import Data.List
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.
(Or, we could sing all that instead.)
2.2 Quicksort in Haskell
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]
= []
qsort [] :ps) = qsort [small | small <- ps, small < p] ++
qsort (p++
[p] | large <- ps, large >= p] qsort [large
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, although we actually run it with cabal repl
.
(Exercise!)
End of Day 1
3 Today’s Notes 2024/09/06
- Office hours start Monday. We’ll fill out the list of office hours with more times soon. See Piazza
- Keep working on the In-class Exercises 1
- Quiz 1: Friday September 20 (during class time)
- Assignment 1 will release soon.
- Before next class:
- Finish the Lecture 1 part of the in-class exercises
- work through the rest of CIS 194 Lecture 1
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?!
(Exercise!)
Try infResults
at the ghci
REPL. What happens? And.. why didn’t it happen before we typed infResults
?
Pause for a moment. A friend wrote this Java code:
public static void loop() {
int i = 0;
while (i < 10) {
System.out.println("going with i:");
System.out.println("i++"); // oops: that's "i++", not i++
}
}
Does that run forever when we compile the code? When we run the program but don’t call loop()
?
So, why does infResults
seem more uncomfortable?
3.1 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
3.2 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.3 Summary
We had a whirlwind tour of many parts of Haskell. What did we see?
Haskell:
- Uses (beautiful?) pattern-matching to make decisions.
- Has polymorphic types similar to interfaces and generics in Java, but (perhaps?) easier to use and more powerful.
- Supports list comprehensions that can be quite elegantly mathematical.
- Uses lazy evaluation and caching to delay unnecessary computations and enable the use of huge or infinite structures.
Learning unusual languages like Haskell and really trying to use them as intended forces you to expand the mechanisms and habits you have for translating your intent into code. That, in turn, is perhaps the most powerful tool you have as a software designer!
4 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:
-- Putting parentheses (round brackets) around an infix operator
-- gives us the prefix version of it. So, 3 + 4 and (+) 3 4 are the same.
(++) :: [a] -> [a] -> [a]
++) [] ys = ys
(++) (x:xs) ys = x : xs ++ ys (
Let’s redefine this in Prolog. We’ll call it append
.
4.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 move robots 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 among three values where the third is the what you would get if you appended the first two.
End of Day 2
5 Today’s Notes 2024/09/09
- Be sure to check out the Canvas site to get to the course website and to get signed up for our tools, like Piazza.
- Office hours have begun to fill out on Piazza
- Finish the Lecture 1 part of the In-class Exercises 1 and start on Lecture 2!
- Quiz 1: Friday September 20 (during class time)
- Assignment 1 is out! Try starting at least a bit of it! There are tests for many different parts on PL.
- Before next class: Read the Haskell Wikibook’s chapter on variables and functions. (That link starts with local definitions like
where
, as you’ll likely be able to skim the portion above that point.)
6 Back to our Lecture!
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 (and that’s one element down!), andappend
is true for the rest ofXs
,Ys
, and the rest ofZs
. The syntax[X|Xs]
is similar to the syntax(x:xs)
in Haskell.
6.1 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). Finally, if you don’t need the right side at all, just put a period after the right parenthesis: ).
% Fill in the blanks on append/3 so that append(Xs, Ys, Zs)
% is true if appending together Xs and Ys results in Zs.
, Ys, Ys).
append([]X|Xs], Ys, [X|Zs]) :- append(Xs, Ys, Zs). append([
(Exercise!)
6.2 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 me 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.)
6.3 Summary
Prolog is perhaps even stranger than Haskell. Again, our goal is to streeeeetch your mind as you translate intent into code. Lean into it!
7 Appendix
Monday Morning Haskell released an 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. Interestingly, we didn’t evaluate most of the values within the list, but we did need to get as far as the list’s full structure and the last element’s value.↩︎