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]))