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) computes 3 - 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:
\[3 * (1 + 4) - 12\]

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:
    1. A value (number, bool, string, symbol)
    2. 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 called cond, which is a multi-way if 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))
  • 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 concatenates hd to the list tl
  • For example, we can construct a list of elements 1, 2, 3 by applying cons 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