An example
Racket (as opposed to Typed Racket) is a dynamically typed language, which means that type errors are not detected until the erroneous code is actually executed. For example, in Racket, we might define a function for squaring numbers like so:
;; 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 error when we run the program:
> (sq 1 2)
sq: arity mismatch;
the expected number of arguments does not match the given number
expected: 1
given: 2
> (sq "hi")
*: contract violation
expected: number?
given: "hi"
argument position: 1st
In this simple example, waiting until we run the program to find the error is not a big deal, but in larger programs such errors can be hidden from immediate detection. Suffice it to say that programs that contain undetected errors are something like ticking time bombs. For example, consider the following (erroneous) function
;; f : Boolean Number Number -> Number
;;
(define (f flg a b)
(cond
[flg (sq a b)] ;; type error: sq should be applied only to one argument
[else 1]))
Testing this function might not expose the error, since the error occurs in 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 actual code, instead of as just a
comment. Returning to our sq
function, we would write it in Typed
Racket as follows:
#lang typed/racket
(require "../include/cs151-core.rkt")
(: sq : Number -> Number)
;; return the square of a number
;;
(define (sq x)
(* x x))
The very 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. Following that is a require
expression that customizes Typed Racket for this course. This line will
also be included in all of your programs. Following that is the definition of
the sq
function. The definition is now three lines, where the first line is
a type annotation (instead of a comment) that tells the system what
the type of the sq
function is (i.e., a function from Number
to Number
).
The buggy function f
from above is now written in Typed Racket as
follows:
(: f : Boolean Number Number -> Number)
;; test program
;;
(define (f flg a b)
(cond
[flg (sq a b)]
[else 1]))
But when we compile this function, we will receive a type error message
because Typed Racket will notice that the use of sq
has too many arguments.
Type Checker: could not apply function; wrong number of arguments provided expected: 1 given: 2 in: (sq a b)
Basic types
Typed Racket defines a very large collection of basic types. We will only use a small subset of them in this course. Operations on these types are described below.
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 negative), and how large their computer representation is. For this course, we use a small subset of these types, which are effectively organized in a hierarchy. We list them here in order of containment (i.e., each type contains all of the previous types):
-
Natural
is the type of non-negative integers. Because this type is not closed under subtraction (i.e.., subtracting two naturals may produce a negative integer), we rarely use it in this course.
-
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 because real numbers can require infinite precision to represent, but computers use finite amounts of storage to hold their values.
-
Number
is the type of all numbers, including complex numbers.
Other basic types
-
Boolean
is the type of logical values. There are exactly two of these; we write them
#t
and#f
. (They may also be writtentrue
andfalse
, but, as we will see later,#t
and#f
are to be prefered.) -
String
is the type of strings of characters, i.e., text, and are enclosed in double quotes, as in
"chocolate"
and"vanilla"
. -
Symbol
is the type of symbols, which are sequences of characters preceded by a single quotation mark. For example,
'red
,'red*
,'*
,'abc-def
, and'+abc
are all symbols. While superficially similar to strings, symbols require significantly less internal storage than strings, are more efficient, and are suitable when textual information is helpful but full-fledged strings are not needed.
Function types
Functions in Typed Racket (and 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 consumes an Integer and
produces a String is written
Integer -> String
The type of a function that consumes an Integer and a Boolean and produces a String is written like this:
Integer Boolean -> String
Typed Racket also supports a prefix syntax for functions types
that is more Scheme-like. In this alternate notation, we
write the ->
, then the argument types, and then the result type.
Using this syntax, the previous examples are written
-> Integer String
-> Integer Boolean String
Note that a function can only ever have one output type, so "where the arrow goes" when using the prefix notation is always unambiguous. While this syntax is more in keeping with the syntax of Racket, we prefer the infix syntax as it matches the syntax of contracts for functions in the textbook and is easier to read.
Type annotations
We can declare the type of a variable using the syntax
(: name : type)
This explicit association, in code, between a name and a type is called type ascription (or sometimes a type annotation). Type ascriptions are checked by the Typed Racket compiler.
(: n : Integer)
Having asserted that n
must be an Integer, the program is allowed
only to bind n
to values of type Integer.
Typed Racket accepts a number of different syntaxes for types and type annotations. As noted above, function types can be written using a prefix notation; e.g.,
(: sq : (-> Number Number))
(: f : (-> Boolean Number Number Number))
One can also leave off the second :
.
(: n Integer)
(: sq (-> Number Number))
(: f (Boolean Number Number -> Number))
Common built-in operations
Typed Racket has a very large collection of built-in functions, including arithmetic operators, string operations, comparisons, etc. Here we describe the commonly used operations on the basic types that were introduced above.
Operations on numbers
Racket uses the same arithmetic operator symbols found in most programming
languages: +
for addition, -
for subtraction and negation,
*
for multiplication, and /
for division. Since Racket uses
a prefix-operator syntax, these operations are not restricted to just two
operands.
The types of these operations are also complicated by the fact that they work on
all numeric types, that you can operate on different mixes of argument types,
and that the Natural
type is not closed under subtraction, and that the
Natural
and Integer
types are not closed under division.
The comparison operators for numeric values also use standard symbols:
<
for less-than, <=
for less-than-or-equal, =
for equal,
>=
for greater-than-or-equal, and >
for greater-than.
As with the arithmetic operations, these operators have variable arity.
For example, the expression
(< x y z)
is true only when both \(x < y\) and \(y < z\).
(: ceiling : Real -> Real)
(: floor : Real -> Real)
(: sqrt : Real -> Real)
(: exact-ceiling : Real -> Integer)
(: exact-floor : Real -> Integer)
(: quotient : Integer Integer -> Integer)
(: remainder : Integer Integer -> Integer)
(: round : Real -> Real)
(: exact-round : Real -> Integer)
(: number->string : Number -> String)
(: min : Real Real * -> Real)
(: max : Real Real * -> Real)
(: odd? : Integer -> Boolean)
(: even? : Integer -> Boolean)
(: negative? : Real -> Boolean)
You will notice that the type of
The syntax |
Operations on booleans
The most important operations on Boolean values are conditionals, which we describe in a later section. We can test equality of booleans using
(: boolean=? : Boolean Boolean -> Boolean)
and we can negate a boolean value using
(: not : Boolean -> Boolean)
Operations on strings
(: string-append : String * -> String)
(: string<=? : String String String * -> Boolean)
(: string<? : String String String * -> Boolean)
(: string=? : String String String * -> Boolean)
(: string>=? : String String String * -> Boolean)
(: string>? : String String String * -> Boolean)
Operations on symbols
(: symbol->string : Symbol -> String)
(: symbol=? : Symbol Symbol -> Boolean)