Learning Racket
- This course assumes a strong background in programming: it should not be your first course involving serious programming
- The first week of the course will be dedicated to getting up to speed in Racket
- I am not assuming anyone knows Racket (though, I am sure some of you do). The goal of this week is to practice “bootstrapping” yourself in a new programming language
- I will give you examples, talk through high-level ideas; you will likely need to do some learning on your own in order to do the homeworks. Learn by immersion!
- These notes are not meant to be exhaustive or authoritative; you are encouraged to rely on the Racket guide to fill in any gaps.
Simple data
- Enter the following code into the interactive window of DrRacket:
> 10 ; comments in Racket start with a semicolon
10
> "hello"
"hello"
> 'hello
'hello
> 10.45
10.45
> #t
#t
- After you hit “enter”, the code is run by Racket and the result of running the program is immediately printed.
- This form of interacting with the programming environment is often called a read-eval-print loop (REPL)
- In Racket, every expression evaluates to a value
- There is no “return” statement: all code is a possibly nested expression
- This differs from many languages you may be familiar with (Python, C, Java)
Calling functions
- To call a function in Racket we use the following syntax:
(function-name arg1 arg2 ... argn)
- There are many built-in functions in Racket. Here are a few that work with numbers:
> (+ 1 2)
3
> (* 4 5)
20
> (+ 1 (- 3 4))
0
- Subtraction isn’t quite what you are probably used to. The syntax
(- 3 4)
computes3 - 4
. - This s-expression syntax can feel unwieldy and unfamiliar, but it does have benefits: it is unambiguous and easy to parse.
- For instance, we all learned PEMDAS in grade school: it lets you parse statements like:
into an unambiguous evaluation of $(3 * (1 + 4)) - 12$.
With s-expressions, we don’t have to worry about remembering this convention (or, any other convention involving order of operations). The parenthesis are “required” and always there.
- You have now seen (almost) the entire syntax of Racket that we will use!
- For the purposes of this class, every Racket program is either:
- A value (number, bool, string, symbol)
- A function call enclosed in parenthesis
- This is a real benefit: Racket has a very lightweight syntax, which lets us focus on semantics
Conditional execution
if
is a built-in function in Racket that takes 3 arguments: the guard, the then branch, and the else branch
> (if #t 10 'oops)
10
> (if #f 10 'oops)
'oops
if
is just a function in Racket, so it can easily be nested with other Racket code:
> (+ (if #t 10 20) 30)
40
- It is common to chain together many
if
conditionals in a row. Racket provides a convenient built-in function for handling this calledcond
, which is a multi-wayif
that executes the first “arm” that evalutes to#t
:
> (cond
[#f 10]
[#f 30]
[#t 20]
[#t 50])
20
- Note that in Racket, you can use square brackets
[]
interchangeably with parenthesis()
- By convention, for
cond
we wrap each arm in square brackets.
Equality
- Equality is an important built-in function that tests if two values are equal to each other:
> (equal? #t 10)
#f
> (equal? #t #t)
#t
> (equal? 10 24)
#f
- There are quite a few different subtle definitions of equality. See https://docs.racket-lang.org/reference/Equality.html. We will discuss this in some more detail later; for now, use
equal?
by default in your code. - For more detail on Racket’s syntax, see this section of the guide
Defining global variables and functions
- Most programming languages have a notion of variables and functions
- In racket we declare a global variable using the
define
built-in function:
> (define x 10)
> x
10
> (+ x x)
20
- You can define your own functions in Racket using the built-in
define
function as follows:
> (define (add1 x) (+ x 1))
> (add1 10)
11
- Multi-argument functions can be defined by providing more than one argument in the definition:
> (define (my-add x y) (+ x y))
> (my-add 10 20)
30
- Now we have enough language features to start defining some interesting programs. Let’s define the factorial function, which is defined recursively:
- if $n = 0$, then
factorial n = 1
- Otherwise,
factorial n = n * (factorial (n-1))
- if $n = 0$, then
- We can implement this using recursion:
; factorial : int -> int
; assumes n is non-negative
; returns 1 if n = 0, n * (factorial (n - 1)) otherwise
(define (factorial n)
(if (equal? n 0) 1 (* n (factorial (- n 1)))))
- You can place this function in the definitions window of DrRacket and press “run”. This will load the function into your REPL, where you can call it:
> (factorial 2)
2
> (factorial 3)
6
> (factorial 4)
24
- Note the comments before this function. All of the functions you write in class should be documented this way.
- You should add these comments to all of your functions before asking for help on an assignment from a TA
- The first line is the type signature: it documents what kind of data the function expects and returns. We do not have a strict requirement for how you write this (we will cover this more in the types module); do your best, and follow our examples.
- The second line lists any assumptions about the input argument that your function expects to hold.
- The third line gives a purpose statement that describes what the intended goal of the function is. This describes the function’s behavior in English. There is not hard-and-fast rule on how to write purpose statements.
- See this section of the guide for more details.
Testing your code
- We should test that our above factorial function satisfies its specification on a few examples
- You will not receive help on your assignment
- To do this, we will use Racket’s built-in testing capabilities. Add the following line to your definitions window and hit
run
:
#lang racket
(require rackunit)
- This loads the
rackunit
testing facilities into your environment. - Now you can write some test cases that check that your factorial function is correctly implemented. The
check-equal?
function takes two arguments and checks that they both evaluate to the same value:
(check-equal? (factorial 0) 1)
(check-equal? (factorial 2) 2)
(check-equal? (factorial 3) (* 3 2))
(check-equal? (factorial 4) (* 4 (factorial 3)))
- You should add tests for all of your functions. To receive help during office hours, you should have at least 3 tests written for all functions that you define.
- See here for the documentation on
rackunit
Lists and pattern matching
- Lists are built out of two constructors:
'()
, the empty list value(cons hd tl)
, the list constructor that concatenateshd
to the listtl
- For example, we can construct a list of elements
1, 2, 3
by applyingcons
three times:
> (cons 1 (cons 2 (cons 3 '())))
'(1 2 3)
- Note the syntax
'(1 2 3)
, which is read “quote one two three”. This is how Racket renders lists; more on that later - It is tedious to type
cons
all the type so there are a number of short-hand ways to describe lists in Racket:
> (list 1 2 3)
'(1 2 3)
> '(1 2 3)
'(1 2 3)
- There are a number of useful built-in functions for lists; you can see a full list here
- Here are some examples of some useful ones:
> (define my-list '(1 2 3))
> (empty? my-list)
#f
> (length my-list)
3
- Now that we’ve built lists, we need a way of destructing them. To do this, we will use the built-in
match
function:
> (define my-list '(1 2 3))
> (match my-list
['() "empty!]
[(cons hd tl) "not empty!])
- Now we can define some interesting functions involving lists! Here is one that sums all of the elements of a list:
; sum-list: int list -> int
; returns the sum of all elements in the list
(define (sum-list l)
(match l
['() 0]
[(cons hd tl) (+ hd (sum-list tl))]))
(check-equal? (sum-list '()) 0)
(check-equal? (sum-list '(1 2 3)) 6)
User-defined data-types
- Most interesting programs implement their own custom data-types.
- An example you will see in the homework is a binary tree. We can build binary trees in Racket as follows:
; type tree =
; | node of tree * tree
; | leaf of number
(struct node (l r) #:transparent)
(struct leaf (x) #:transparent)
- The
#:transparent
syntax is boilerplate: it tells the DrRacket REPL that this struct can be printed. If you’re curious, see here - Now we can build binary trees:
> (leaf 10)
(leaf 10)
> (node (leaf 20) (leaf 30))
(node (leaf 20) (leaf 30))
- To destruct your structs and manipulate them, you should use pattern matching:
> (define my-tree (node (leaf 10) (leaf 20)))
> (match my-tree
[(leaf n) n]
[(node l r) l])
(leaf 10)
- Experiment with matching to get a feel for it
- Here is the detailed documentation for pattern matching if that is helpful. There are many more examples.
Local variables
- Local variables are declared with the built-in
let
function:
> (let [(x 10)] (+ x 20))
30
- There are a few different syntactic forms of
let
that offer different conveniences while programming; we will introduce those later as-needed. If you are curious see this part of the reference
Next time
- You should now be ready to do most of the first homework!
- It is highly recommended that you start on this homework before next lecture.
- Next time, we will see some more examples of writing Racket code, reasoning about inductive datatypes, and we will discuss first-class functions and lambdas