Welcome to CPSC 312!

Lecture #1
Complete the associated in-class exercises.

Table of Contents

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


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:

  1. If the list is empty, simply return it.
  2. 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 just p, 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 [] = []
qsort (p:ps) = qsort [small | small <- ps, small < p] ++ 
               [p] ++ 
               qsort [large | large <- ps, large >= p]

What did we see? Haskell:

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


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]
genList cap seed = map (`mod` cap) (randoms (mkStdGen seed))

We could try out qsort with:

infResults :: [Int]
infResults = qsort (genList 10000 0)

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]
randList n = take n (genList n 0)

bigList :: [Int]
bigList = randList 1000000

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:

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

6 Back to our Lecture!

The Haskell code above kind of tells us our cases. append(Xs, Ys, Zs) is true when:

  1. Xs is just the empty list and Ys = Zs.

  2. Xs and Zs start with the same element (and that’s one element down!), and append is true for the rest of Xs, Ys, and the rest of Zs. 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.
append([], Ys, Ys).
append([X|Xs], Ys, [X|Zs]) :- append(Xs, Ys, Zs).

(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:

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 [] = []
qsort (p:ps) =
   qsort [s | s <- ps, s < p]
   ++ [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]
genList cap = map (`mod` cap) . randoms . mkStdGen

  1. 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.↩︎

  2. ghc did not cache last bigList because we didn’t attach that to a name. It did cache the bigList because in demanding the value of last bigList, we also demanded that it evaluate bigList 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.↩︎