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
andfalse
(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 |
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:
-
If the predicate check succeeds, the argument variable has type
Posn
-
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 |
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 |
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:
-
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).
-
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 |
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 |
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.