Introduction

This year, for the first time, we will be using Typed Racket as the programming language in CMSC 15100. Typed Racket provides automated checking for many kinds of common errors in your code, but it also introduces some new complexities that are not covered in the text book. The purpose of these notes is to provide supplementary information that will aid you in the programming assignments.

As we introduce new language features in class, we will extend this document.

For further reading, you can consult The Typed Racket Guide and The Typed Racket Reference.

An example

Racket is a dynamically typed language, which means that type errors are not detected until the erroneous code is actually executed. For example, we might define a function for squaring numbers.

;; sq : Number -> Number
;; return the square of a number
;;
(define (sq x) (* x x))

If we then attempt to apply it to two arguments or to the wrong kind of argument, we get a runtime type error

> (sq 1 2)
sq: expects only 1 argument, but found 2
> (sq "hi")
*: expects a number as 1st argument, given "hi"
>

In this simple example, waiting until runtime to find the error is not a big deal, but in larger programs such errors can be hidden from immediate detection. For example, consider the following (erroneous) function

;; f : Boolean Number Number -> Number
;;
(define (f flg a b)
  (cond
    [flg (sq a b)] ;; type error
    [else 1]))

Testing this function might not expose the error, since the error occurs on only one branch of the conditional. For example:

> (f #f 1 2)
1

In this simple example, an additional test case is sufficient to detect the error, but, in general, it may involve a significant amount of testing to check all of the potential dynamic type errors.

In Typed Racket, we write the type signature (or contract) of a function as part of the program’s syntax, instead of just as a comment. Returning to out sq function, we would write it in Typed Racket as follows:

#lang typed/racket

(: sq : Number -> Number)
;; return the square of a number
;;
(define (sq x) (* x x))

The first line is a directive that tells the Racket system that we are using the Typed Racket language. You will include this directive in all of your programs. The definition of the sq function is still three lines, but the first line is now a type annotation that tells the system what the type of the sq function is (a function from Number to Number).

(: f : Boolean Number Number -> Number)
;; silly test program
;;
(define (f flg a b)
  (cond
    [flg (sq a b)]
    [else 1]))

Basic types

Typed Racket defines a very large collection of basic types. We will only use a small subset of them in this course.

Numbers

Typed Racket divides numbers into many different types based on properties such as whether they are rational, integral, or not, their sign (positive or negitive), and how large their computer representation is. For this course, we use a small subset of these types, which are effectively organized in a heirarchy. We list them here in order of containment (_i.e., each type contains all of the previous types):

  • Natural

    is the type of the natural numbers (i.e., the integers that are greater or equal to zero).

  • Integer

    is the type of the integers.

  • Exact-Rational

    is the type of the rational numbers (i.e., numbers that can be written as a fraction).

  • Real

    is the type of the real numbers. Computer representations of the reals are necessarily inexact.

  • Number

    is the type of all numbers, including complex numbers.

Other basic types

  • Boolean

    is the type of logical values. It has two values true and false (these may also be written #t and #f).

  • Symbol

    is the type of symbols, which are sequences of identifier characters preceded by a single forward quotation mark. For example,

'red   '+   'abc-def  '+abc

are all examples of symbols.

  • String

    is the type of text strings.

Function types

Functions in racket take zero or more arguments and return a single result. We write the type of a function by listing the argument types, followed by the symbol and the result type. For example, the type of a function that takes an Integer argument and a Boolean argument and returns a string is written as follows:

Integer Boolean -> String

Type annotations

As we have seen above, we can declare the type of a variable using the syntax

(: name : type)

Typed Racket accepts a number of different syntaxes for types and type annotations. First, function types can be written using a prefix notation; e.g.,

(: sq : -> Number Number)
(: f : -> Boolean Number Number Number)

While this syntax is more in keeping with the syntax of Racket, we prefer the infix syntax as it matches the syntax of type contracts in the text book and is easier to read. One can also leave off the second ":", but in that case function types need an extra set of parentheses.

(: sq (-> Number Number))
(: f (Boolean Number Number -> Number))

Function contracts

In this course (and in the text) each function definition is preceded by a contract and a header comment. Most of the contracts that we will use in this class can be captured by types and checked by the computer. The informal syntax used by the text for contracts is a close match to the formal syntax of types used by Typed Racket; and can be copied with only small changes. For example, the text gives the following contract, header, and purpose statement for the profit function:

;; profit : number -> number
;; to compute the profit as the difference between revenue and costs
;; at some given ticket-price
(define (profit ticket-price) ...)

The use of to specify an incomplete function body is not supported in Typed Racket, so we give the function’s definition when we translate:

(: profit : Number -> Number)
;; to compute the profit as the difference between revenue and costs
;; at some given ticket-price
(define (profit ticket-price)
  (- (revenue ticket-price)
     (cost ticket-price)))

Notice that the only change that we had to make was to change "number" to "Number".

Variable definitions

Variable definitions should also be proceeded by a type annotation. The syntax is essentially the same as for functions. For example:

(: pi : Real)
(define PI 3.14159)

Testing

The student languages in DrRacket have a builtin testing mechanism called cehck-expect. This mechanism is also available in Typed Racket, but using it requires a bit more work. First, at the top of your definitions window (i.e., the beginning of the program) you will need a require directive as follows:

#lang typed/racket

(require typed/test-engine/racket-tests)

And at the end of your program, you need a command to run the tests:

(test)

There are two forms of test that you will use in this class. The check-expect form tests to see if an expression evaluates to an expected result.

(check-expect expr expected-expr)

For example, we can add tests for the sq function that we defined above:

(: sq : Number -> Number)
;; return the square of a number
;;
(define (sq x) (* x x))

(check-expect (sq 5) 25)
(check-expect (sq -1) 1)

When computing with real numbers, the results are often inexact. In that situation we use the check-within form to test if an expression evaluates to within a given delta of an expected result.

(check-within expr expected-expr delta-expr)

For example

(check-within (sq (sqrt 2)) 2 0.001)

Note that the test function runs all of the tests defined up to its invocation. Therefore, you should only invoke it once after all of the tests have been defined.

Querying types

It is possible to query the type of an identifier using DrRacket’s REPL using the :print-type function:

> (:print-type sq)
(-> Number Number)

Notice that Typed Racket prints function types using the prefix syntax. == Defining more types

Typed Racket provides mechanisms for defining both product and sum types.

Structs

As in the Chapter 6 of the textbook, Typed Racket provides the define-struct special form. The main difference is that unlike the form described in the book, in Typed Racket we must specify the types of the structure components. For example:

(define-struct Posn
  ([x : Real]
   [y : Real])
  #:transparent)

The other noticable difference is the #:transparent option, which tells Typed Racket to make this a transparent type (as opposed to an opaque type). Transparent struct types can be tested for equality in a check-expect and the contents of a transparent struct value will be printed in the REPL. While there are good software engineering principles for making types opaque, we will always use transparent structs in this course.

Note

The book describes a predefined posn type that is similar to the one we defined above. This builtin type is not available in Typed Racket.

The define-struct definition defines a new type named Name. It also generates the definitions of a bunch of operations on the struct type. For example, the definition of Posn generates functions with the following types:

(: make-Posn : Real Real -> Posn
(: Posn? : Any -> Boolean : Posn)
(: Posn-x : Posn -> Real)
(: Posn-y : Posn -> Real)

You may notice that the type of Posn? has three parts (Any, Boolean, and Posn). The first two are the same as any other function type and indicate that the predicate takes any value and returns a boolean. The third part, after the :, is a filter that tells the typechecker two things:

  1. If the predicate check succeeds, the argument variable has type Posn

  2. If the predicate check fails, the argument variable does not have type Posn

Predicates for all built-in types are annotated with similar filters that allow the type system to reason about predicate checks. This mechanism is called occurrence typing and allows functions, such as the following, to typecheck:

(: posn-x? : Any -> Integer)
(define (posn-x? p)
  (if (Posn? p) (Posn-x p) 0))

since the typechecker knows that p has type Posn in the ‘then’ branch of the if. This mechanism is restricted to arguments that are variables; testing the result of an expression and then expecting the

Note

Typed Racket also provides the struct special form, which has the same syntax as define-struct. The main difference is that the struct declaration introduces a function with the same name as the struct type.

Unions

Union types provide a mechanism to define a type that is one of several distinct choices. For example, we might define a representation of colored circles, where the available colors are red, green, or blue. We can use symbols to represent the distinct colors; i.e., 'red, 'green, and 'blue. A symbol in Typed Racket is its own type, as illustrated by the following code snippet:

(: R : 'red)
(define R 'red)

This kind of type is called a singleton type, since it is a type that has exactly one value. By unioning multiple singleton types, we can describe enumeration types. For example, our color type is the union of 'red, 'green, and 'blue. Two other useful singleton types are #t and #f (as one might guess, Boolean is the union of #t and #f).

We can define a function that maps colors to their names as follows:

(: color-to-string : (U 'red 'green 'blue) -> String)
;; convert a color to a string
;;
(define (color-to-string c)
  (cond
    [(symbol=? c 'red) "red"]
    [(symbol=? c 'blue) "blue"]
    [else "green"]))

Here the identifier U in the type stands for union. Also notice that the declared type of color-to-string guarantees that the else clause of the conditional will always be green.

Note

The order in which types appear in a union does not matter. For example, the type (U 'a 'b) is the same as the type (U 'b 'a).

Our definition of colored circles is as follows:

;; The represention of a colored circle
;;
(define-struct Circle
  ([center : Posn]
   [radius : Real]
   [color : (U 'red 'green 'blue)])
  #:transparent)

Now we can define a blue unit circle centered at (2, 3) as follows:

(: blue-circ : Circle)
(define blue-circ
  (make-Circle
    (make-posn 2 3)
    1
    'blue))

We can also define unions of all kinds of types. For example, we might a representation of colored rectangles:

;; The represention of a colored axis-aligned box
;;
(define-struct Rect
  ([center : Posn]
   [width : Real]
   [height : Real]
   [color : (U 'red 'green 'blue)])
  #:transparent)

and then define a function for getting the color from a shape

(: shape-color : (U Circle Rect) -> (U 'red 'green 'blue))
;; Get the color of a shape.
;;
(define (shape-color shape)
  (cond
    [(Circle? shape) (Circle-color shape)]
    [else (Rect-color shape)]))

This function is an example of the use of occurrence typing in a conditional. In the first clause of the conditional, the Circle? predicate determines that the shape variable has the type Color in the expression part and does not have that type in the rest of the conditional. Because the typechecker knows that the variable shape must be either a Circle or a Rect, it knows that the type of shape must be Rect in the else clause.

Type definitions

Since type expressions can get quite large and complicated, it is helpful to introduce names for such types. This objective fits nicely with the concept of a data definition that is introduced in the textbook (Section 6.4).

For example, instead of writing

;; A color is one of 'red, 'blue, or 'green.

we can give an explicit definition using the define-type construct

(define-type Color (U 'red 'blue 'green))

We can also give a name to our shape type:

(define-type Shape (U Circle Rect))

Subtyping

Union types give rise to a notion of subtyping; i.e., the idea that one type is contained in another. For example, the type 'red is contained in the type Color, so we say that 'red is a subtype of Color. Intuitively, you can think of subtyping in Typed Racket as corresponding to the subset relation of the sets of values in the types. For example, the value 'red is contained in the set of Color values {'red, 'green, 'blue}.

Another example of subtyping that you have already experienced is the hierarchy of numeric types. The type Natural is a subtype of Integer, Integer is a subtype of Exact-Rational, etc.

Subtyping comes into play when Typed Racket checks the types of arguments against the type of a function in an application. If an argument expression has a type that is a subtype of the expected type, then it is okay. For example, when checking the expression

(color-to-string 'red)

we have an argument of type 'red, which is a subtype of the expected type.

Patterns and pattern matching

Pattern matching is an elegant, useful feature of modern programming language design. Pattern matching is part of common idioms in functional programming languages including Standard ML, Haskell and their derivatives. Pattern matching is also part of the design of Typed Racket.

Programs that make use of pattern matching are cleaner, easier to read and more pleasant to work with than their pattern-free counterparts. Pattern matching is not addressed in How to Design Programs, but since it offers so many advantages, and no disadvantages, and is so well-suited to the style of programming taught in this course, we include it in our curriculum.

Typed Racket’s pattern matching offers a rich collection of syntactic forms, only some of which are included in this document. Note that you will find many other pattern matching forms in the language documentation, but, for the purposes of this course, you only need a few of the available forms.

Pattern matching basics

As conditional expressions are written with cond, pattern matching expressions are written with match. A match expression has the following general form:

(match exp_0
  [pat_1 exp_1]
  [pat_2 exp_2]
  ...
  [pat_k exp_k])

Match evaluation is similar to cond evaluation. First the expression exp_0 is evaluated to a value we’ll call v. Then v is compared against each pattern in turn, in the order given: pat_1, then pat_2, and so on, up to pat_k. If v matches pat_1, then the value of the whole expression is exp_1, and evaluation within the match goes no further. If pat_1 doesn’t match, then v is checked against pat_2, and the value is exp_2. Evaluation continues from pattern to pattern until a match is found (or none is).

Consider this match expression:

(match (+ 1 1)
  [2 "A"]
  [3 "B"])

In this case, (+ 1 1) is evaluated to 2. This is compared first against the first pattern in order, which is a constant pattern 2. This is a match, so the value of the whole expression is the string "A". With the following alteration, the value of the expression is "B":

(match (+ 1 2)
  [2 "A"]
  [3 "B"])

There are many useful kinds of patterns besides constant patterns: one such kind is variable patterns. A variable pattern consists of a variable name, and a) always succeeds and b) gives a name to the value matched. Consider this example:

(match (* 10 3)
  [n (+ n 1)])

The value of this expression is 31. Why? The expression immediately following match evaluates to 30. Then 30 is compared against the variable pattern n, which succeeds in matching (as variable patterns always do) and furthermore binds the name n to the expression’s value. When the expression (+ n 1) is evaluated, n is bound to 30, and we finally arrive at 31.

Because variable patterns always match, they can be used as catch-all or default patterns in a pattern match, playing a role not entirely unlike what else does in the last branch of a cond. For example:

(match (+ 8 9)
  [0 "zero"]
  [n "not zero"])

In this expression, the match of evaluated 17 against the constant pattern 0 fails. The match against n necessarily succeeds, and the value of the expression altogether is the string "not zero".

The variable pattern n in the last example is a suitable candidate for a wildcard pattern, written with the underscore character _. Recall that a variable pattern both matches a value and binds a name to it, but in this case (and many cases in practice) the name is not needed, since n is not subsequently used. In such cases as this, a wildcard pattern, informally known as a "don’t care" pattern, is a good choice. A wildcard pattern always succeeds in matching, like a variable pattern does, but doesn’t bind; that is, it associates no name with the matched value.

(match (+ 8 9)
  [0 "zero"]
  [_ "not zero"])

The underscore serves not only to catch any expression that falls past the constant pattern 0, but it also expresses clearly that the particular value of the matching expression is of no interest, only the fact that it did not match any previous pattern.

Matching structures and nested patterns

Matching against structures is a convenient way to, first, identify the particular struct under consideration, and also to name the individual components in the structure and thereby eliminate calls to selectors.

Define a point in 2-space as follows:

(define-struct Point
  ([x : Real]
   [y : Real])
  #:transparent)

Now assume there exists an expression of type Point bound to a variable p, and consider the following match:

(match p
  [(Point x y) (+ x y)])

This expression performs the (admittedly contrived) operation of adding together the x and y components of the point p. The pattern (Point x y) serves several purposes: Point ensures that p is actually a Point struct, and furthermore the names x and y are bound to its two components. Syntactically, this is a real bargain, although it will take practice and experience to appreciate why. Without pattern matching, a similar expression looks like this:

(+ (Point-x p) (Point-y p))

On the whole, the latter expression is no longer (in number of characters) than the first; however, in the part of the expression where the action is — namely, where x and y are added together — the former expression is a clean (+ x y), while the latter is cluttered up with selectors (Point-x and Point-y). Pattern matching often has this clarifying effect by separating the deconstruction of a compound value from computing with its elements.

Using nested patterns with nested structures pushes the advantages of structure matching still further. Consider a line segment structure containing two points as follows:

(define-struct Seg
  ([a : Point]
   [b : Point])
  #:transparent)

Now we consider the following (contrived) computation: to subtract the product of the segments y components from the product of its x components. We can write the expression with nested patterns like this:

(match s
  [(Seg (Point ax ay) (Point bx by)) (- (* ax bx) (* ay by))])

Compare that expression to this one that uses selectors:

(- (* (Point-x (Seg-a s)) (Point-x (Seg-b s)))
   (* (Point-y (Seg-a s)) (Point-y (Seg-b s))))

In the second expression, the selectors overwhelm the syntax and make reading the expression (comparatively) difficult; the matched alternative above (- (* ax bx) (* ay by)) is crystal clear by comparison.

Pattern matching in function definitions

Pattern matching is especially useful in the definition of functions. Here is a function definition using only selectors:

(: q1? : Point -> Boolean)
;; test whether the point is in the first quadrant
(define (q1? p)
  (and (positive? (Point-x p))
       (positive? (Point-y p))))

When we rewrite the function using a match, the code is not shorter, but its main expression is free of selectors and hence clearer:

(: pat-q1? : Point -> Boolean)
;; reimplementation of q1? using pattern matching
(define (pat-q1? p)
  (match p
    [(Point x y)
     (and (positive? x)
          (positive? y))]))

With nested structures, nested pattern matching cleans up and clarifies the computation to an even greater extent. The following two functions compute the same property of a line segment; compare the syntax of one to the other.

(: p1 : Seg -> Boolean)
(define (p1 s)
  (< (* (Point-x (Seg-a s)) (Point-y (Seg-b s)))
     (* (Point-x (Seg-b s)) (Point-y (Seg-a s)))))

(: p2 : Seg -> Boolean)
(define (p2 s)
  (match s
    [(Seg (Point ax ay) (Point bx by))
     (< (* ax by) (* bx ay))]))

Matches and disjoint unions

Recall the definition of the Shape type from above.

(define-type Shape (U Circle Rect))

We can use pattern matching to write functions that work over union types like Shape. For example, the shape-color function can be written using pattern matching as

(: shape-color : Shape -> Color)
;; Get the color of a shape.
;;
(define (shape-color shape)
  (match shape
    [(Circle _ _ col) col]
    [(Rect _ _ _ col) col]))

We use wild cards for the parts of the shapes that we are not interested in.

Matching multiple expressions with match*

Typed Racket also provides a mechanism for matching against multiple values with multiple patterns. For example, say we wanted a function to test if two shapes are the same shape and color. We could write that as

(: same-shape-and-color? : Shape Shape -> Boolean)
;; test if two shapes are the same kind and have the same color.
;;
(define (same-shape-and-color? shape1 shape2)
  (match* (shape1 shape2)
    [((Circle _ _ col1) (Circle _ _ col2)) (symbol=? col1 col2)]
    [((Rect _ _ _ col1) (Rect _ _ _ col2)) (symbol=? col1 col2)]
    [(_ _) #f]))

Pattern-matching gotchas

You have to be careful to avoid using the identifiers true, false, empty in patterns. These identifiers are variables (not literals), which means that they will match any value. For example, the following function will always return false.

(: flip : Boolean -> Boolean)
(define (flip b)
  (match [true false] [false true]))

The correct way to write it is to use the #f and #t literals in the patterns:

(: flip : Boolean -> Boolean)
(define (flip b)
  (match [#t false] [#f true]))

Recursive types

Recursion lets us repeat a computation an arbitrary number of times, but we also want to be able to represent arbitrary collections of data. For example, you might want to represent the students in a class and write computations to compute average grades etc.

Bespoke lists

Suppose that we want to have lists of shapes?

Informally, we might think of a list of shapes as being either - an empty list - a pair of a shape and a list of shapes

This is an example of an inductively defined set. We can define such sets in Typed Racket as follows:

(define-type Shape-List
  (U 'empty-shape-list Shape-List-Cons))
(define-struct Shape-List-Cons
  ([shape : Shape]
   [rest : Shape-List])
  #:transparent)

The Shape-List type is an example of an inductive type. I.e., a type that is defined by one or more base cases (e.g., 'empty-shape-list) and one or more inductive cases (e.g., make-Shape-List).

Inductive types are naturally processed by recursive functions. When writing recursive functions over inductive types, we usually follow a standard pattern of base cases for the base cases in the type defintiion and recursive cases for the inductive cases in the type definition. For example, say we want to write a recursive function to count the number of shapes in a list. We start with the header and a skeleton:

(: count-shapes : Shape-List -> Natural)
;; count the number of shapes in a list
;;
(define (count-shapes cl)
  (match cl ...)

We then add a clause to the match for our base case: the empty list, which has length 0:

(define (count-shapes cl)
  (match cl
    ['empty-shape-list 0]
    ...)

We also need a recursive case for the non-empty list. In that situation, the length of the list will be one plus the length of the rest of the list. The complete function is

(define (count-shapes cl)
  (match cl
    ['empty-shape-list 0]
    [(Shape-List-Cons _ r) (+ 1 (count-shapes r))]))

Polymorphic functions and types

Just as functions allow us to abstract over specific values, polymorphism allows us to abstract over types.

Consider the following definition of pairs of integers with a function for swaping values:

(define-struct Two-Ints ([fst : Integer] [snd : Integer]) #:transparent)

(: swap-ints : Two-Ints -> Two-Ints)
(define (swap-ints pair)
  (match pair [(Two-Ints a b) (make-Two-Ints b a)]))

And this similar definition of pairs of booleans:

(define-struct Two-Bools ([fst : Boolean] [snd : Boolean]) #:transparent)

(: swap-bools : Two-Bools -> Two-Bools)
(define (swap-bools pair)
  (match pair [(Two-Bools a b) (make-Two-Bools b a)]))

These types and functions have the same structure, but we had to write them twice in order to get the types right.

When we have a family of function and/or type definitions that are parametric in their definition, we can use polymorphic definitions theat allow us to write one implementation that can be used at many types. The idea is very similar to writing functions as a way to parameterize dynamic behavior, but in this case we are parameterizing static behavior.

For example, we can define a polymorphic representation of pairs

(define-struct (A B) Two-Things ([fst : A] [snd : B]) #:transparent)

(: swap : (All (A B) (Two-Things A B)-> (Two-Things B A)))
(define (swap pair)
  (match pair [(Two-Things a b) (make-Two-Things b a)]))

In the define-struct, we have introduced two type variables A and B, which represent the types of the first and second elements (resp.). This declartion defines a type constructor Two-Things. Type constructors are functions that make a type out of other types (we have already seen two builtin type constructors: U and ->). We can now represent pairs of integers as the type (Two-Things Integer Integer) and pairs of Booleans as (Two-Things Boolean Boolean).

We also use type variables in the definition of the polymorphic swap function. The syntax

(All (A B) (Two-Things A B)-> (Two-Things B A))

is called universal quantification and it means "for all types A and B a function from (Two-Things A B) to (Two-Things B A)". If we apply swap to a value of type (Two-Things Integer Boolean)

(swap (make-Two-Things 42 #t))

we will get back a value of type (Two-Things Boolean Integer).

(make-Two-Things #t 42)

Type instantiation

Sometimes it is necessary to instantiate a polymorphic function at a specific type. We use the inst special form to do this operation. The syntax is

(inst id ty1 ty2 ...)

where id is the polymorphic function and the ty1, ty2, … are type arguments (one per variable in id's type). For example, we can instantiate the type of swap by giving it two type arguments:

(: swap-ib : (Two-Things Integer Boolean) -> (Two-Things Boolean Integer))
(define swap-ib
  (inst swap Integer Boolean))

Lists

We have seen an example of a bespoke list (the list of shapes). We could define other specific kinds of lists, but it would be painful to have to write essentially the same type definitions and functions everytime we wanted to make a list of values.

Fortunately, Typed Racket has a generic (or polymorphic) list type. Most functional languages, including Racket, use lists as their primary mechanism for dealing with sequences. Therefore, the support lists as a builtin mechanism.

Let us consider the simple example of representing lists of booleans.

;; A (Listof bool) is either
;; - empty, (written '()) or
;; - (cons b bs), where b is a bool and bs is a (listof bool)

The empty list + '()+ and the list constuctor cons come with the language. There are corresponding predicates empty? and cons? with type any → bool. There is also a predifined variable empty that stands for the empty list.

(: empty : Null)
(: empty? : Any -> Boolean : Null)
(: cons  : (All (T) T (Listof T) -> (Listof T)))
(: cons? : Any -> Boolean : (Listof Any Any))

The type Listof is a type constuctor. The function cons is polymorphic. Discuss.

Actually, cons has a slightly more complicated type because it is also the pair constructor.

Let’s write down a few lists of booleans.

empty
(cons true empty)
(cons true (cons false empty))
(cons false (cons true (cons false empty)))

We can also construct lists using the quotation syntax:

'()
(list)
(list true)
'(#t)
'(#t #f)
(list false true false)

But we have to be careful here, because '(false true) is not the same as '(#f #t)

The functions first and rest are selectors on cons structures.

(: first : (All (T) (Listof T) -> T))
(: rest  : (All (T) (Listof T) -> (Listof T)))
(: second : (All (T) (Listof T) -> T))
...
(: eighth : (All (T) (Listof T) -> T))

There are also more historical names: car, cdr.

(: car : (All (T) (Listof T) -> T))
(: cdr  : (All (T) (Listof T) -> (Listof T)))

Functions on lists

Typed Racket provides various standard operations on lists as polymorphic functions. We can compute the length of a list with

(: length : (All (X) (Listof X) -> Integer))

(check-expect (length '()) 0)
(check-expect (length '(1 2 3)) 3)

Reverse the order of a list using

(: reverse : (All (X) (Listof X) -> (Listof X))

(check-expect (reverse '(1 2 3)) '(3 2 10))

Two or more lists can be concatenated using the append function.

(: append : (All (X) (Listof X) * -> (Listof X)))

(check-expect (append '(1 2 3) '(4) '(5 6))
              '(1 2 3 4 5 6))

We can extract an element from a list by its index using

(: list-ref : (All (X) (Listof X) Integer -> X))

(check-expect (list-ref '(1 2 3) 0) 1)
(check-expect (list-ref '(1 2 3) 1) 2)
(check-expect (list-ref '(1 2 3) 2) 3)

Notice that indexing starts at 0. Specifying a negative index or one that is too large results in an error. Lastly, the make-list function creates a list of the given length

(: make-list : (All (X) (Integer X -> (Listof X)))

(check-expect (make-list 3 "hi") (list "hi" "hi" "hi"))

Higher-order functions on lists

Typed Scheme provides a number of higher-order functions that operate on lists:

(: map : (All (X Y) (X -> Y) (Listof X) -> (Listof Y)))

(: foldl : (All (X Y) (X Y -> Y) Y (Listof X) -> Y))

(: foldr : (All (X Y) (X Y -> Y) Y (Listof X) -> Y))

(: build-list : (All (X) Integer (Natural -> X) -> (Listof X)))

(: filter : (All (X) (X -> Boolean) (Listof X) -> (Listof X)))

(: andmap : (All (X) (X -> Boolean) (Listof X) -> Boolean))

(: ormap : (All (X) (X -> Boolean) (Listof X) -> Boolean))

(: argmin : (All (X) (X -> Real) (Listof X) -> X))

(: argmax : (All (X) (X -> Real) (Listof X) -> X))

Some more Racket features

Local definitions

Local definitions provide two important features:

  1. they provide local names for intermediate values, which is useful for breaking complicated expressions into a series of simpler ones and provides a way to share the results of computations (instead of computing the same expression multiple times).

  2. they provide a way to limit the scope (or visibility of variables such as helper functions).

The syntax of a local definition is

(local { ... defs ... } expr)

where definitions appear between the curly braces and the expr is evaluated to produce the result. For example:

(local
 {(define n1 (- n 1))}
 (* n1 n1))

Locals can also be used to define helper functions (including their type contracts). For example, here is a tail recursive version of the factorial function written using a helper.

(: fact : Natural -> Natural)
(define (fact n)
  (local
   {(: fact-aux : Natural Natural -> Natural)
    (define (fact-aux i acc)
      (if (<= i n)
          (fact-aux (+ i 1) (* i acc))
          acc))}
  (fact-aux 1 1)))

Notice that the fact-aux function references n, which is bound as a parameter to the outer function fact. This is an example of using lexical scoping to avoid needing extra parameters to the helper function.

Anonymous lambdas

The lambda expression provides a way to define a non-recursive function without giving it a name. The basic syntax is

(lambda ([param-1 : ty-1] ... [param-n : ty-n]) expr)

where the param-+i are the function parameters with the corresponding declared types. Anonymous functions are particularly useful in combination with higher-order functions, such as +map and foldl. For example, we can compute the sum of the squares of a list of numbers as follows

(: sum-of-sqrs : (Listof Real) -> Real)
(define (sum-of-sqrs xs)
  (foldl
   (lambda ([x : Real] [acc : Real]) (+ acc (* x x)))
   0
   xs))

(check-expect (sum-of-sqrs '()) 0)
(check-expect (sum-of-sqrs '(1 2 3)) 14)

Lambda expressions are also quite useful for writing curried functions. For example, a curried form of addition can be written as

(: curried-add : Integer -> (Integer -> Integer))
(define (curried-add a)
  (lambda ([b : Integer]) (+ a b)))

Curried functions are useful when combined with other higher-order functions. For example, using curried-add, we can use map to increment the elements of a list by some amount:

(: list-inc : Integer (Listof Integer) -> (Listof Integer))
(define (list-inc delta xs) (map (curried-add delta) xs))

Vectors

Lists represent sequences as an inductive structure, which makes adding elements (to the front) constant time, but accessing random elements $O(n)$ time. Racket also provides the builtin type constructor Vectorof for representing sequences as contiguous arrays of data. This representation gives $O(1)$ access, but requires copying the whole vector to add an element.

Similar to the list function, Tthe vector function can be used to construct a vector from elements and to pattern match against vectors of fixed size.

(vector 1 2 3)

Racket also provides a quotation syntax for writing vector literals

'#(1 2 3)

Typed Racket provides various standard operations on vectors as polymorphic functions. These operations are similar to the corresponding list functions.

We can compute the length of a vector with

(: vector-length : (All (X) (Vectorof X) -> Integer))

Two or more vectors can be concatenated using the vector-append function.

(: vector-append : (All (X) (Vectorof X) * -> (Vectorof X)))

(check-expect (vector-append '#(1 2 3) '#(4) '#(5 6))
              '#(1 2 3 4 5 6))

We can extract an element from a vector by its index using

(: vector-ref : (All (X) (Vectorof X) Integer -> X))

(check-expect (vector-ref '#(1 2 3) 0) 1)
(check-expect (vector-ref '#(1 2 3) 1) 2)
(check-expect (vector-ref '#(1 2 3) 2) 3)

Notice that indexing starts at 0. Specifying a negative index or one that is too large results in an error. We can build a vector using a generator function

(: build-vector : (All (X) (Integer (Natural -> X) -> (Vectorof X)))

(check-expect (build-vector 3 number->string) '#("0" "1" "2"))

Lastly, the make-vector function creates a vector of the given length filled with the given value

(: make-vector : (All (X) (Integer X -> (Vectorof X)))

(check-expect (make-vector 3 "hi") (vector "hi" "hi" "hi"))

Here are other vector functions that you might find useful:

(: list->vector : (All (X) (Listof X) -> (Vectorof X)))
;; convert a vector to a list

(: vector->list : (All (X) (Vectorof X) -> (Listof X)))
;; convert a list to a vector

(: vector-map : (All (X Y) (X -> Y) (Vectorof X) -> (Vectorof Y)))
;; maps a function across a vector to produce a new vector

(: vector-filter : (All (X) (X -> Boolean) (Vectorof X) -> (Vectorof X)))
;; filter a vector using the given predicate

(: vector-argmin : (All (X) (X -> Real) (Vectorof X) -> X))
;; return the vector element for which the function returns the smallest value

(: vector-argmax : (All (X) (X -> Real) (Vectorof X) -> X))
;; return the vector element for which the function returns the largest value

(: vector-count : (All (X) (X -> Boolean) (Vectorof X) -> Integer))
;; count the number of vector elements that satisfy the predicate

Imperative programming

Upto now, we have exclusively used immutable values in our programs, but Typed Racket, like many functional programmming languages, also supports imperative programming with mutable state. In this class, we will use mutable vectors and mutable structs. Typed Racket also allows mutable variables, but do not use them.

Mutable vectors

The basic operation for updating a mutable vector is

(: vector-set! : (All (a) (Vectorof a) Integer a -> Void))

The Void return type means that this function does not return any value.

Note

The use of the ! suffix is a Racket naming convention used to signify functions that work imperatively.

Warning

The vector quotation syntax produces vectors that are immutable, which means that you will get a runtime error if you try to operate on them with vector-set!.

Sequencing operations

With operations, such as vector=-set!, that return Void, our code will often become a sequence of updates. We use the expression form

(begin exp1 exp2 ... expn)

for this purpose; it has the effect of executing exp1, exp2, … in turn. Its result is the value of expn. For example, we can use it to define a function that swaps two elements of a vector:

(: swap! : (All (A) (Vectorof A) Integer Integer -> Void))
;; swap the ith and jth elements of the vector
(define (swap! v i j)
  (local
    {(define tmp (vector-ref v i))}
    (begin (vector-set! v i (vector-ref v j))
           (vector-set! v j tmp))))

Sometimes we need an expression of type Void (e.g., to make the types of a conditional match). We can use the void function, which takes no arguments, for this purpose:

(: void : -> Void)

For example, the following expression swaps the +i+th and +j+th elements of a vector when the +i+th element is greater than the +j+th.

(if (> (vector-ref v i) (vector-ref v j))
    (swap! v i j)
    (void))

Mutable structs

In addition to mutable vectors, we will also use mutable structs. These are defined by adding the #:mutable option to the struct definition as in the following example:

(define-struct Pt
  ([x : Real] [y : Real])
  #:transparent
  #:mutable)

When we include the #:mutable option in a struct declaration, Typed Racket will generate additional functions for modifying the struct (one for each declared field). In our Pt example, we get two additional operations.

(: set-Pt-x! : Pt Real -> Void)
(: set-Pt-y! : Pt Real -> Void)

that can be used to update in place the x and y fields of the struct. For example, here is a function that moves a point by the given offsets.

(: pt-move! : Pt Real Real -> Void)
(define (pt-move! p dx dy)
  (begin
    (set-Pt-x! p (+ (Pt-x p) dx))
    (set-Pt-y! p (+ (Pt-y p) dy))))

Input/Output

We only use simple input/output (I/O) operations in this class. Typed Racket provides a function for reading a line of text from the user.

(: read-line : -> (U String EOF))
(: eof-object? : Any -> Boolean : EOF)

This function returns either a string of the input text or the special EOF (end of file) value. You can use eof-object? to test for EOF.

The display function will write any type of value to the screen.

(: display : Any -> Void)

Since display does not force a newline after its output, you can use the newline function to force a linebreak.

(: newline : -> Void)

We can combine display and newline together to implement a helpful debugging function.

(: show : (All (A) A -> A))
(define (show x)
  (begin
    (display x)
    (newline)
    x))

This function displays its argument and returns it, which means that you can wrap it around any expresssion to see what the result of the expression is.