Lecture 2: Programming in Plait
Table of contents
- A bit more on Plait
- The
cond
form - Lists
- Custom data structures
- Type annotations
- Local names and local functions
- (Optional if time) First-class Functions
- Goals: become comfortable programming in Plait
- Lists
- User-defined data-structures
local
andlet
- A bit on first-class functions
Practice solving problems recursively with Plait
- Some logistics:
- Gradescsope: should have received email, if not use code EJGJPX to sign up
- Schedule change: President’s day
- Questions about Homework problem #2:
- On the homework, one of the exercises replaces “y” with “ies”, “Y” with “Ys”. We won’t test this capital case. See Piazza for a brief clarification.
A bit more on Plait
- The Plait definition window contains a sequence of Plait expressions that are evaluated in sequence
- The Plait interaction window evaluates a Plait expression as soon as it is entered
- Plait’s types are inspired by ML
Plait resources
- “How to Design Programs” will be useful for refreshing/learning programming in a functional style: https://htpd.org
- See the Plait cheatsheet on Canvas.
- The Plait documentation: https://docs.racket-lang.org/plait/Tutorial.html
The cond
form
Reference: https://htdp.org/2023-8-14/Book/part_one.html#%28part._sec~3acond%29
Today we will encounter a lot of patterns that involve nested
if
-expressions. These are often best expressed using thecond
form:
(define (next traffic-light-state)
(cond
[(string=? "red" traffic-light-state) "green"]
[(string=? "green" traffic-light-state) "yellow"]
[(string=? "yellow" traffic-light-state) "red"]))
Note: Brackets make cond lines stand out and are customarily used uere, but it is fine to use
( ... )
in place of[ ... ]
.What happens if the input string is none of the above? In this case, it may be useful to have a default case:
(define (next traffic-light-state)
(cond
[(string=? "red" traffic-light-state) "green"]
[(string=? "green" traffic-light-state) "yellow"]
[(string=? "yellow" traffic-light-state) "red"]
[else (error 'invalid-input "oops")]))
- Note:
cond
branches are run in order
Lists
- Following https://htdp.org/2023-8-14/Book/part_two.html
Constructing lists
- The empty list is a constant written
'()
(just like#t
is the true constant) - We can add something to a list using
cons
:
(cons 1 '())
- (Listof Number)
'(1)
- We can build longer lists by chaining together
cons
:
> (cons 1 (cons 2 (cons 3 '())))
- (Listof Number)
'(1 2 3)
- This can be inconvenient to do, so we can use the built-in
list
function instead if we want to:
> (list 1 2 3)
- (Listof Number)
'(1 2 3)
- More convenience: we can append two lists together using
append
:
> (append (list 1 2) (list 3 4))
- (Listof Number)
'(1 2 3 4)
List types
- Lists must contain values of the same type. This is invalid:
> (list 1 "hello" 3)
racket-mode-repl::9-16: typecheck failed: Number vs. String
sources:
"hello"
(list 1 "hello" 3)
1
- What is the type of the empty list? Let’s see:
> '()
- (Listof '_a)
'()
- The type
(Listof '_a)
says that this is a list of some undetermined type'_a
, which we will find later; the symbol'_a
is a symbolic type that Plait doesn’t know yet. For instance, in this program Plait will infer that the symbolic type must be String:
> (cons "hello" '())
- (Listof String)
'("hello")
- We will discuss more about how Plait knows which types to use later on in the course, but for now, we can program with these types and Plait will infer what they are for us.
Destructing lists
- Getting the first element of a list:
> (first (list 1 2 3))
- Number
1
- Similarly getting the second:
> (second (list 1 2 3))
- Number
2
- Getting the rest of the list:
> (rest (list 1 2 3))
- (Listof Number)
'(2 3)
Runtime errors
- Stop: What happens if we do this?
> (rest (list))
- We get a runtime error:
> (rest (list))
- (Listof '_a)
rest: contract violation
expected: (and/c list? (not/c empty?))
given: '()
- We will need to avoid calling
rest
on empty lists. For this we will often useempty?
, which is true if a list is empty and false otherwise:
> (empty? (list))
- Boolean
#t
> (empty? (list 1 2))
- Boolean
#f
The List Recipe
- Now we have the pieces we need to perform operations on lists
- Sample problem:
You are working on the contact list for some new cell phone. The phone’s owner updates and consults this list on various occasions. For now, you are assigned the task of designing a function that consumes this list of contacts and determines whether it contains the name “Matthias.”
- We start by writing a template:
; determines whether "Matthias" is on a-list-of-names
(define (contains-matthias? names)
#false)
- Then we add some tests, looking to cover all possible corner cases:
(test (contains-matthias? '()) #false)
(test (contains-matthias? (cons "Find" '()))
#false)
(test (contains-matthias? (cons "Matthias" '()))
#true)
(test
(contains-matthias?
(cons "A" (cons "Matthias" (cons "C" '()))))
#true)
- Stop: run the program. What happens?
- All tests fail, but hopefully nothing else (no syntax errors, no type errors). Now what? We need to fill in the body of our function.
- A list can either be (1) empty, or (2) of the form
(cons hd tl)
. So, we begin by filling in the body of our function with these two cases:
; determines whether "Matthias" is on a-list-of-names
(define (contains-matthias? names)
(cond
[(empty? names) ...]
[#t ]))
- Now we have to handle these two cases.
- Q: What happens in the first? A: Return
#f
, pretty easy - Stop: In words, what should we do in the second case?
- Q: What happens in the first? A: Return
- Solution:
; determines whether "Matthias" is on a-list-of-names
(define (contains-matthias? names)
(cond
[(empty? names) #f]
[#t (if (equal? (first names) "Matthias") #t (contains-matthias? (rest names)))]))
List exercises
- Write a function
all-matthias?
that returns#t
if every element of the list is “Matthias”. Your function should have type((Listof String) -> Boolean)
. - Write a function
twice-matthias?
that returns#t
if the list contains “Matthias” at least twice. Your function should have type((Listof String) -> Boolean)
.
Custom data structures
Defining custom data structures
- Most interesting programs have their own custom data-types.
- Suppose we have the following example scenario:
We want to model two kinds of animals: tigers, who have a certain color and number of stripes; and snakes, who have a certain color, numeric weight, and preference of food.
- We can define this data-type in Plait in the following way:
> (define-type Animal
(tiger [color : Symbol]
[stripe-count : Number])
(snake [color : Symbol]
[weight : Number]
[food : String]))
- Note: the spaces around the colons are syntax and are required
This the elements
tiger
andsnake
of the typeAnimal
are called variants: a value of typeAnimal
must either be asnake
or atiger
The data contained in each variant is called a member (example:
color
is a member of thetiger
variant)- When we define a new type this way, Plait gives us a few functions for free. First, type constructors for each of the variants:
> (tiger 'blue 10)
- Animal
(tiger 'blue 10)
> (snake 'green 2 "students")
- Animal
(snake 'green 2 "students")
- Second, tests for each variant:
> (tiger? (tiger 'blue 10))
#t
- Third, accessors for members of each variant:
> (tiger-color (tiger 'red 10))
- Symbol
'red
- If we call the accessor on an invalid variant we get a runtime contract error:
(tiger-color (snake 'green 10 "student"))
- Symbol
tiger-color: contract violation
...
The type-case
form
Clearly while programming with custom data-types, we quite often want a way to quickly (1) determine which type of variant we have been given, and (2) access its members and computate with them. Moreover, we want to ensure that this is exhaustive and we handle all variants
The
type-case
form lets pattern-match on your custom data-types:
> (define (get-color animal)
(type-case Animal animal
[(tiger color stripe-count) color]
[(snake color weight food) color]))
> (get-color (tiger 'green 12))
- Symbol
'green
- This syntax is quite unusual and you probably haven’t seen it before. The general form of
type-case
is:
(type-case Type expr
[(pattern1) body1]
[(pattern2) body2]
...
[(patternN) bodyN])
- Each
pattern
matches exactly 1 of the type constructors. - Each
body
can reference the names found in the matched pattern. - The type-case construct will helpfully inform you if you forget a case:
> (define (get-color animal)
(type-case Animal animal
[(tiger color stripe-count) color]
))
. type-case: syntax error; probable cause: you did not include a case for the snake variant, or no else-branch was present in: (type-case Animal animal (tiger (color stripe-count) (#%expression color)))
- We can also use
type-case
to safely match on lists:
(define (my-length l)
(type-case (Listof 'a) l
[empty 0]
[(cons a b) (+ 1 (my-length b))]))
Programming with custom data-types
- Use
type-case
to solve the following problems:
- Re-implement the
all-matthias?
program above, but this time usingtype-case
to deconstruct the List - Write a
all-tigers?
that takes a list ofAnimal
as an argument and returns#t
if they are all tigers and#f
otherwise. - Write a function
all-green?
that takes a list ofAnimal
as an argument and returns#t
if they are all'green
animals and#f
otherwise. - Write a function
make-green
that takes a list ofAnimal
as an argument and returns that same list of animals but with all the colors replaced by'green
.
Type annotations
Often it is useful to annotate our programs with types in order to prevent bugs and aid in documentation / code understandability. Plait lets us do this in two ways.
- First, we can use inline type annotations that let us associate each identifier in a definition with a type, as follows:
(define mynum : Number 10) (define l : (Listof Number) (list 1 2 3)) (define (myadd [x : Number] [y : Number]) : Number (+ x y))
- You can include some or all of the types in the above annotation:
(define (myadd x [y : Number]) : Number (+ x y))
- Alternatively, you can add a top-level type description, which goes before the definition:
(l : (Listof Number))
(define l (list 1 2 3))
(myadd : (Number Number -> Number))
(define (myadd x y) (+ x y))
- Notice in the above:
- The signature
Number Number -> Number
says “takes two numbers as arguments and returns a number” - The type signature
(Listof Number)
requires the parenthesis: you can think ofListof
as a function that takes a Type as an argument
- The signature
Exercise
- Go through all of your previously defined functions and add type annotations.
Local names and local functions
- Often when writing interesting code it is useful to define local variables that are not visible from outside of the current function
- We can accomplish this with the
local
form, which causes all definitions inside of it to only be visible (in-scope) inside the body of thelocal
form - Examples:
> (local [(define x 10) (define y 20)] (* x y))
200
> x
x: free variable while typechecking
- The general syntax of
local
is:
(local [(definition1) (definition2) ... (definitionN)] body])
Each
definitionN
is required to be a definition, and thebody
is an expression from which the definitions are in-scope (meaning visible)It is often useful to define helper functions using the
local
form:
> (define (twice-matthias-local? l)
(local [(define (count-matthias l)
(type-case (Listof String) l
[(cons hd tl) (if (equal? hd "matthias") (+ 1 (count-matthias tl)) (count-matthias tl))]
[empty 0]))]
(> (count-matthias l) 1)))
The let
and letrec
forms
- The
let
form is a convenient shorthand for defining local variables:
> (let [(x 10) (y 20)] (* x y))
200
> x
x: free variable while typechecking
- The general syntax of
let
is:(let [(id1 expr1) (id2 expr2) ... ] body)
- The
let
syntax assumes that all identifiers do not mutually refer to each other.letrec
relaxes this constraint:
> (let [(x 10) (y (* x x))] (* x y))
x: free variable while typechecking in: x
> (letrec [(x 10) (y (* x x))] (* x y))
- Number
1000
- Question: can you come up with a way to automatically translate a program that uses
let
into one that only useslocal
? How about translatingletrec
intolet
?
(Optional if time) First-class Functions
- In Plait, functions are values
> (lambda (x) (+ x x))
- (Number -> Number)
- They can be called, bound to an identifier, and passed as arguments:
> ((lambda (x) (+ x x)) 10)
20
> (define double (lambda (x) (+ x x)))
> (double 10)
20
> (define (apply-twice f x) (f (f x)))
> (apply-twice double 3)
12