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 written true and false, 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 min and max is given as

Real Real * -> Real

The syntax Real * in these types means that these functions expect zero or more Real arguments (after the initial argument). We call these functions uniform variable-arity functions. While you will not write such functions in this course, you will frequently use them.

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)
Last updated 2020-01-10 19:38:40 -0600
Table of Contents | Back to CS151 Home