Skip to content

Instantly share code, notes, and snippets.

@kmicinski
Created February 2, 2026 07:54
Show Gist options
  • Select an option

  • Save kmicinski/3219342bd9fa3aac5feff74288b41e14 to your computer and use it in GitHub Desktop.

Select an option

Save kmicinski/3219342bd9fa3aac5feff74288b41e14 to your computer and use it in GitHub Desktop.
#lang racket
;; CIS352 -- The Algebra of Programming
;; Spring 2026 -- Kristopher Micinski
;; Introduction:
;;
;; Students often ask (in CIS352): "why learn Racket, a language with
;; an arcane syntax that I might not use outside of this class?" It is
;; a very reasonable question, and today we will answer it directly:
;;
;; We use Racket because Racket is the language that lets us most
;; rapidly experiment with and compute over *term algebras*.
;;
;; Using algebra will allow us to reason rigorously about code,
;; enabling us to (a) prove that certain fragments of code are *equal*
;; but also (b) giving us a way of gaining intuition for what the code
;; is doing as we evaluate the program. The essential purpose of
;; algebra is to be able to perform *equational reasoning*:
;; substituting equals by equals to iteratively establish the equality
;; of increasingly-complex code via simple, easily-understood local
;; conversions.
;;
;; Computers are extremely complex, and the typical programmer's
;; mental model of the program's execution probably involves (at
;; least) some local variables, a stack, and memory (e.g., the
;; heap). While this is not an unreasonable abstraction (although it
;; is arguably a bit archaic for truly modern hardware), it relies on a
;; lot of complexity which you (likely) do not fully understand. Our
;; goal is to have you look at languages that are so simple, you can
;; understand their execution completely, with *absolutely zero*
;; semantic ambiguity. To do that, we will start by connecting back to
;; our high school algebra classes.
;;
;; We all intuitively understand:
;;
;; f(x,y) = x^2 + cos(y/x)
;;
;; In high-school algebra, we learn how to manipulate algebraic
;; expressions, and in particular we know how to do substitution,
;; e.g.:
;;
;; f(z+k, z^2) = (z+k)^2 + cos((z^2)/(z+k))
;;
;; High-school algebra then teaches us to "simplify" expressions into
;; canonical forms. E.g., the "FOIL" rule, i.e., the identity:
;;
;; (x + y)^2 = x^2 + 2xy + y^2
;;
;; A significant amount of high-school algebra is about building our
;; intuition for how to simplify these algebraic expressions into
;; canonical forms.
;; □ -- Term Algebras ("Term algebras are finite trees.")
;; Definition: Term Algebra (not to be confused with high-school
;; algebra, etc.)
;;
;; Assume T is a signature: a set of constructors (names) along with
;; associated arities. The term algebra over T is the set of terms
;; which may be formed by applying constructors (respecting their
;; associated arities) to other terms in the algebra. We may construe
;; zero-arity constructors as the algebra's "constants."
;;
;; For example, take C = {'0 with arity 0, '1 with arity 0, '+ with arity 2}
;; terms include: 0, 1, 0 + 1, 1 + (0 + 1), (0 + 0) + 1, etc...
;;
;; We wrote examples in infix style to be intuitive, pedantically, we
;; might write:
;; 0(), 1(), +(0(),1()), +(1(),+(0(),1()))
;;
;; Without loss of generality, we will represent all function calls in
;; prefix form. By doing this, we see a central advantage of using
;; Racket for the course: Racket's lists enable us to *trivially*
;; represent term algebras
;;
;; You can think of term algebras as "raw, uninterpreted terms."
;;
;; (Rhetorical) Question: Why are they so closely related to Racket?
;; Answer: Racket makes term algebras *immediately visible*
;; 1. Constructors: cons / '()
;; 2. Functions defined via case analysis: match
;; 3. Recursion mirrors the inductive structure of terms
;; QuickAnswer
;; Now, let's have you write this up in Racket: Write a type
;; predicate, `add-expr?`, which characterizes the set of expressions
;; over the constants 1/0 and the two-argument +:
(define (add-expr? e)
(match e
[0 'todo] ;; 0 is an add-expr?
[1 'todo] ;; 1 is an add-expr?
;; Hint: possible to use a predicate pattern: ,(? p? e0)
[`(+ ,e0 ,e1) 'todo] ;; (+ e0 e1) is an add-expr? if e0 / e1 are add-expr?s
[_ #f]))
;; Now, given an expression, e, we may write a Racket function that
;; evaluates the expression `e` down to a *number*. In fact, if we
;; construe `add-expr?` to define a programming language, then we are
;; writing our first *interpreter*. We will use recursion and matching
;; to build our interpreter, following a common style we will learn
;; about later. We will not focus extensively on interpreters in
;; today's class, but we *will* using algebraic data types to define
;; the *abstract syntax* of programming languages frequently.
;; (add-expr->number '(+ 1 (+ 0 (+ 1 0)))) => 2
(define (add-expr->number e)
(match e
[0 'todo]
[1 'todo]
[`(+ ,e0 ,e1) 'todo]))
;; Given any signature T (a set of constructors and associated
;; arities), it is possible to write a type predicate using match.
;;
;; Example:
;;
;; Let the algebra bool? be formed by the following constructors:
;;
;; - true -- of arity 0
;; - false -- of arity 0
;; - and -- of arity 2
;; - or -- of arity 2
(define (bool? e)
(match e
['true #t]
['false #t]
[`(and ,(? bool? e0) ,(? bool? e1)) #t]
[`(or ,(? bool? e0) ,(? bool? e1)) #t]
[_ #f]))
;; QuickAnswer
;; Define a function that converts a bool? to a Racket boolean? (i.e.,
;; just #t/#f).
(define (bool?->racket b)
'todo)
;; □ -- The natural numbers, as a term algebra
;; We now define the natural numbers as a term algebra whose signature
;; is 0 with arity 0 and S (successor) with arity 1:
(define (nat? n)
(match n
['0 #t]
[`(S ,(? nat? n)) #t]
[_ #f]))
(define zero '0)
(define three '(S (S (S 0))))
;; Convert a racket natural to a number
(define (nat->num n)
(match n
['0 0]
[`(S ,n+) (add1 (nat->num n+))]))
;; Assume n0 satisfies nat?, write nat-even?
(define (nat-even? n)
(match n
['0 #t]
[`(S 0) #f]
[`(S (S ,n)) (nat-even? n)]))
;; QuickAnswer
;; *don't* use nat->num or nat-even?
(define (nat-odd? n)
'todo)
;; QuickAnswer
;; Assume n0 and n1 are *both* nat? Now, define (<= n0 n1)
;;
;; Basic approach: use recursion over n0 / n1:
;; - Match n0, if n0 is 0 then return (equal? n1 0)
;; - If n0 not 0, then what do you do...?
;;
;; DON'T use nat->num (which makes this easier)
(define (nat-lte n0 n1)
'todo)
;; □ -- Quasiquoting and Recursive Functions on Algebras
;; Last lecture (and this lecture), we learned about quasipatterns,
;; which allow us to match list-like patterns. We can use quasiquoting
;; to *produce* lists which "drop in" or "splice in" other
;; expressions.
;; For example:
(let ([x 20])
`(,x x ,(+ x 1))) ;; notice, `, *NOT* '
;; This expression produces a list of three elements:
;; - (1) 20, because `x` was *unquoted* (i.e., evaluated, to be dropped in)
;; -> The comma *unquotes*, it says: "evaluate the next expression
;; you read and then drop it in.
;; - (2) x, because x was under a quasiquote and not unquoted
;; -> Unless you unquote, ` acts like '
;; - (3) 21, because ,(+ x 1) says: evaluate (+ x 1), then take its result
;; and drop it in here.
;; If we use quote ' instead of quasiquote `, we will just get the
;; literal expression, and no unquoting is possible:
(let ([x 20]) '(,x x ,(+ x 1))) ;; produces literally '(,x x ,(+ x 1))
;; We will start to *use* quasiquotes pervasively to *produce*
;; algebraic data. For example, we will now define the (add-two n)
;; function.
;; Assume n satisfies nat?
(define (add-two n) `(S (S ,n)))
(add-two '(S (S (S 0)))) ;; '(S (S (S (S (S 0)))))
;; Assume n0, n1 both nat. Define n0 + n1. Algebraically, we say:
;; (plus 0 k) = k
;; (plus (S n) k) = (S (plus n k))
;; Your result must satisfy nat?, and forall n0, n1 which satisfy
;; nat?, and we must have that:
;; (nat->num (plus n0 n1)) = (+ (nat->num n0) (nat->num n1))
(define (plus n0 n1)
(match n0
['0 n1]
[`(S ,n) `(S ,(plus n n1))]))
;; QuickAnswer
;; Idea: recursion on n0
;; (times 0 k) = 0
;; (times (S n) k) = k + (times n k)
;; Remember:
;; (nat->num (times n0 n1)) = (* (nat->num n0) (nat->num n1))
(define (times n0 n1)
'todo)
;; QuickAnswer
;; Now, we use recursion on nat? to define the factorial function:
;; (fac 0) => 1
;; (fac (S n)) => (S n) * (fac n)
;; Remember, we must produce a nat?
(define (factorial n)
'todo)
;; (factorial '(S (S (S (S 0)))))
;; => '(S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S 0))))))))))))))))))))))))
;; (nat->num (factorial '(S (S (S (S 0)))))) => 24
;; □ -- Equality and Canonical Terms (Advanced)
;; It is important to understand that the term algebra is a set of
;; "raw" terms, which are not equated in any way. For example,
;; although we may be tempted to think that `(+ 1 0) and `(+ 0 1)
;; should represent the *same* term (because they both evaluate to 3
;; in Racket), they are distinct terms from the perspective of the
;; term algebra add-expr? Specifically...
(equal? '(+ 1 0) '(+ 0 1)) ;; #f
;; Definition: Quotient Algebra
;;
;; In many settings, it will be desirable to define *equations* over
;; terms. These equations effectively partition the term algebra into
;; equivalence classes. For example, for the algebra add-expr?, we
;; might also consider the following equations:
;; (+ x y) = (+ y x) (plus-comm)
;; (+ 0 x) = x (plus-id-0)
;;
;; From these two rules, we may prove that: (+ x 0) = x
;; - (+ x 0) = (+ 0 x) (applying plus-comm)
;; - (+ 0 x) = x (applying plus-id-0)
;;
;; Recall that = is reflexive, antisymmetric, and transitive
;;
;; When we take a term algebra and add an equivalence relation, we get
;; a *quotient algebra*. It is an esoteric-sounding name, but it
;; involves a key distinction:
;; - The term algebra is just a *raw set of terms*, all of which are
;; just finite trees.
;; - A quotient algebra refines this, equating some of the terms to
;; each other, effectively partitioning the space.
;; For example, while...
(equal? '(+ 0 (+ 1 1)) '(+ 1 (+ 0 1))) ;; #f
;; in Racket, the quotient algebra formed by the two equations above
;; would disagree, instead saying that all of the following are equal:
;; (+ 0 (+ 1 1)) = (+ 1 (+ 0 1)) = (+ 1 1)
;; IMPORTANT: Specific proofs around quotient algebras are a more
;; advanced topic, and you will *not* need to deeply understand them
;; for the course. However, it is important to be able to understand
;; the difference between "structural equality" (i.e., equal?) and
;; "application-specific" equality.
;;
;; In general, it is not possible to decide whether two arbitrary
;; quotient algebra terms are *equal* or not. The "word problem for
;; equational theories" says that it is impossible, *in general*, to
;; decide the equivalence of two terms t0 and t1, under the set of
;; equations E.
;;
;; While we cannot decide the equivalence of arbitrary algebraic
;; formulas, almost all of our day-to-day programming is done in a
;; setting where equality is _trivially_ decidable: for example,
;; deciding the equality of numbers is easy.
;; Compare two add-expr?s for equality
;; Assume: n0 / n1 satisfy add-expr?
(define (add-expr-equal? n0 n1)
(define (add-expr->num e)
(match e
[0 0]
[1 1]
[`(+ ,e0 ,e1) (+ (add-expr->num e0) (add-expr->num e1))]))
(equal? (add-expr->num n0) (add-expr->num n1)))
;; This section was more advanced--let's switch back to using algebra
;; to do programming.
;; □ -- Folding over lists (foldr)
;; We are now ready to use some of the algebra we have developed in
;; this lecture to develop a very special function, `foldr`, which is
;; defined via the following equations:
;;
;; (fold-1) (foldr f z '()) = z
;; (fold-2) (foldr f z (cons hd tl)) = (f hd (foldr f z tl))
;;
;; (foldr f z l) "folds" the function f across the list z with the
;; initial value ("zero") z. By "fold" f across the list, I actually
;; mean "iterate" or "accumulate." It will make more sense once we see
;; an example:
;;
;; (foldr + 0 '(1 2 3)) => 6
;; (foldr + 3 '(1 2 3)) => 9
;; (foldr * 1 '(2 3 4)) => 24
;;
;; Fold's second argument is a "folder" or "updater" function, which
;; takes two inputs:
;; - The next element in the list being inspected.
;; - The current "accumulator" value.
;;
;; In the case of foldr (fold *right*), the folder / updater function
;; is applied from *right to left*, and the initial accumulator value
;; is z (the second argument, "zero"--a sensible default value).
(define (foldr f z l)
(match l
['() z]
[`(,hd ,tl ...) (f hd (foldr f z tl))]))
;; Let's use equational reasoning to show that...
;;
;; (foldr + 0 '(1 2 3)) = 6
;;
;; (foldr + 0 '(1 2 3)) = (foldr + 0 (cons 1 (cons 2 (cons 3 '()))))
;; = (+ 1 (foldr + 0 (cons 2 (cons 3 '())))) (via fold-2)
;; = (+ 1 (+ 2 (foldr + 0 (cons 3 '())))) (via fold-2)
;; = (+ 1 (+ 2 (+ 3 (foldr + 0 '())))) (via fold-2)
;; = (+ 1 (+ 2 (+ 3 0))) (via fold-1)
;; = (+ 1 (+ 2 3))
;; = (+ 1 5)
;; = 6
;; QuickAnswer
;; Using the standard rules of arithmetic for *, +, etc., and also the two rules:
;;
;; (fold-1) (foldr f z '()) = z
;; (fold-2) (foldr f z (cons hd tl)) = (f hd (foldr f z tl))
;;
;; Rigorously prove--via a series of equalities (annotate fold-1/2
;; when appropriate)--the following:
;;
;; (foldr * 1 '(2 3 4)) = 24
'todo-write-proof-in-comments
;; As a more advanced example of how we might use algebra for
;; programming, I want to pose the following equality:
;;
;; forall l. l = (foldr cons '() l)
;;
;; Why is this the case intuitively?
;;
;; Pictorally, here is what (foldr f z l) is doing:
;;
;; (cons x0 (cons x1 (cons x2 ... (cons xn-1 '()) ...)))
;; | | | | | '() maps to z
;; v v v v v
;; ( f x0 ( f x1 ( f x2 ... (f xn-1 z) ) ) )
;;
;; Notice how the fold transforms the list, effectively replacing each
;; `cons` with a call to `f`, and the empty list `'()` by the zero
;; (initial value) `z`.
;;
;; Now, back to our original claim:
;; forall l. l = (foldr cons '() l)
;;
;; Pictorally, this is true because...
;;
;; (cons x0 (cons x1 (cons x2 ... (cons xn-1 '()) ...)))
;; | | | | |
;; v v v v v
;; (cons x0 (cons x1 (cons x2 ... (cons xn-1 '()) ...)))
;; ADVANCED (optional); However, this picture is not a rigorous
;; proof. To make a rigorous proof, we would do induction over the
;; list l, establishing an inductive hypothesis that for all sublists
;; xs of (cons x xs), xs = (foldr cons '() xs), and we would prove
;; that (cons x xs) = (foldr cons '() (cons x xs)) by using (foldr-2)
;; and the induction hypothesis.
;;
;; We will see this later on, when we look at using Lean.
;; It is easy to miss the power of this idea, or see it as abstract
;; pedantic nonsense. In fact, foldr characterizes an extremely common
;; class of functions that process lists by accumulating a result,
;; walking over the list in some order (right to left, as in foldr, or
;; left to right as in foldl).
;;
;; Foldr gives us a general-purpose way to transform lists by walking
;; over each element one at a time and firing a two-argument "updater"
;; function, which takes (a) the next value in the list, and (b) the
;; current accumulator value (which defaults to "zero", foldr's third
;; argument).
;; For example, we can define the function (filter f l) via foldr
(define (filter f l)
(foldr (lambda (next-elt acc) (if (f next-elt) (cons next-elt acc) acc))
'()
l))
;; QuickAnswer
;; Use foldr to add all *even* numbers in the list l
;; (add-evens '(0 1 2 3 4 5)) => 6
(define (add-evens l)
(foldr (lambda (x acc) 'todo)
0
l))
;; Fold is a pervasive construct, popular across many languages. For example:
;;
;; Rust:
;; let sum = vec![1, 2, 3, 4].iter().fold(0, |acc, x| acc + x);
;; Python:
;; sum = reduce(lambda acc, x: acc + x, [1, 2, 3, 4], 0)
;; JavaScript:
;; const sum = [1, 2, 3, 4].reduce((acc, x) => acc + x, 0);
;; C++:
;; std::vector<int> v = {1, 2, 3, 4};
;; int sum = std::accumulate(v.begin(), v.end(), 0)
;; SQL:
;; SELECT SUM(x)
;; FROM (VALUES (1), (2), (3), (4)) AS t(x)
;;
;; Fold is so popular because it represents the following imperative construct:
;;
;; acc = 0
;; for (elt in list.reverse) { acc = f(elt,acc); }
;;
;; In practice, *almost any function over lists can be written using
;; fold*.
;;
;; Also, fold forms the basis for MapReduce, a popular distributed /
;; parallel computing framework:
;; https://www.ibm.com/think/topics/mapreduce
;; QuickAnswer
;; What is the result of...
(foldr (lambda (x acc) (+ acc (- x))) 0 '(10 10 10))
;; Optional (using algebra for proving correctness of code):
;;
;; We will prove: for all l, (filter (lambda (x) #t) l) = l
;;
;; Proof:
;; - First, we show that that:
;; (lambda (next-elt acc) (if ((lambda (x) #t) next-elt) (cons next-elt acc) acc))
;; =
;; cons
;;
;; - Proving this equality is actually a bit tricky, because we have
;; not talked about lambdas, but notice that (lambda (x) #t) will
;; *always* return true, and thus the *guard* is always
;; true. Thus--because the guard is always true--the function is
;; really equal to...
;;
;; (lambda (next-elt acc) (cons next-elt acc))
;;
;; This is *almost* cons, and it is in fact *equal* to cons, in the
;; sense that the above lambda does the same things to the same
;; arguments (i.e., the function is extensionally equal). We will
;; see that they are related via a concept named η-expansion in the
;; λ-calculus.
;; SUMMARY -- Key concepts from this lecture:
;;
;; - A "term algebra" is an infinite set of terms (all individually
;; finite), each of which is formed via the application of
;; constructors according to some signature T (constructors and their
;; arities).
;;
;; - Racket's lists ("S-expressions") naturally represent term
;; algebras in prefix form, and it is easy to express term algebras
;; via Racket type predicates using pattern matching.
;;
;; - Term algebras are all finite trees, and thus syntactic equality
;; is dictated by structural equality (i.e., equal? in racket!).
;;
;; - It is sometimes desirable to extend term algebras with a set of
;; equations. These equations partition the term algebra into
;; equivalence classes, and form a quotient algebra--however, deciding
;; equality of terms in an arbitrary quotient algebra is not possible
;; (undecidable) in general via any algorithm. Instead, we must
;; manually demonstrate a "proof," via a chain of reasoning steps.
;;
;; - Algebra gives us a very powerful tool to reason about equality of
;; programs by transitive term rewriting--each equality in the chain
;; follows from some equation in the system.
;;
;; - Folding over lists is a fundamental looping construct, which
;; allows us to "fold" an "updater" or "folder" function across a
;; list, walking over it from right to left (hence fold*r*) and
;; starting with some initial accumulator zero.
;;
;; - Folds can be used for a wide range of list-processing tasks, we
;; will begin to use them more frequently throughout class.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment