Lecture 2: Programming in Plait

Table of contents

  1. A bit more on Plait
    1. Plait resources
  2. The cond form
  3. Lists
    1. Constructing lists
    2. List types
    3. Destructing lists
      1. Runtime errors
    4. The List Recipe
    5. List exercises
  4. Custom data structures
    1. Defining custom data structures
    2. The type-case form
    3. Programming with custom data-types
  5. Type annotations
    1. Exercise
  6. Local names and local functions
    1. The let and letrec forms
  7. (Optional if time) First-class Functions

  • Goals: become comfortable programming in Plait
    • Lists
    • User-defined data-structures
    • local and let
    • 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 the cond 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 use empty?, 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?
  • 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

  1. Write a function all-matthias? that returns #t if every element of the list is “Matthias”. Your function should have type ((Listof String) -> Boolean).
  2. 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 and snake of the type Animal are called variants: a value of type Animal must either be a snake or a tiger

  • The data contained in each variant is called a member (example: color is a member of the tiger 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:
  1. Re-implement the all-matthias? program above, but this time using type-case to deconstruct the List
  2. Write a all-tigers? that takes a list of Animal as an argument and returns #t if they are all tigers and #f otherwise.
  3. Write a function all-green? that takes a list of Animal as an argument and returns #t if they are all 'green animals and #f otherwise.
  4. Write a function make-green that takes a list of Animal 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 of Listof as a function that takes a Type as an argument

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 the local 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 the body 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 uses local? How about translating letrec into let?

(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