Session 14

An Application: Type Checking

CS 3540
Programming Languages and Paradigms

recursion at all scales
Recursion at all scales. [1]

Where We Are

For the last few weeks, we have been discussing different techniques for writing recursive programs, all based on the fundamental technique of structural recursion. Last time, we applied these techniques in writing a recursive program to answer this question about programs in our little language from Session 12: Does a particular variable occur bound in a given piece of code? Our program, (occurs-bound? var exp), was mutually recursive with the function occurs-free? because the definitions of bound and free occurrences are mutually inductive.

In order to think and write more clearly about the little language, we used a new design pattern, Syntax Procedures, which allowed us to focus on the meaning of our data rather than their implementation in Racket.

Today, we practice by solving a problem or three that put to use many of the ideas we have learned about Racket, functional programming, and recursive programming. These problems will also be more realistic: you'll have to flesh our some of the details yourself.

Exercise types-match?, Part 1

Some of you have noticed that many of the functions you've written this semester have assumed that we could apply a given function to every member of a list. If we can't we won't find out until run-time, because Racket is dynamically typed. Can we do differently? Let's write some functions that enable us to check for valid data before launching into the body of a function.

First, let's design a function named (types-match? types values), where

types-match? should return true if every member of values satisfies the type predicate at the corresponding position in types, and false otherwise. For example:
     > (define type-signature (list number? string?))
     > (types-match? type-signature '(42 "Hello, Eugene"))

To support variable arity, let's add one condition: If there are fewer types than values, the function uses the last predicate in types to validate all of the remaining arguments.

Your tasks:

Work in groups of two or three to think through the problem and create your solutions. Ask for clarification if there is some part of the problem you don't understand.

Solutions to Part 1

Data types:

    values is a standard list: lst ::= () | (any . lst)

    types is too, but...
    - only empty if values is, too
    - otherwise, if we ever run out of types, we use the last type over
    - so may be better to think of it as a list with *at least 1* type
      or pull out first type and then have standard list

    this tells us how to think about our test cases
    (and, later, how to write the code)

Test cases:

    ()() + { true, false } x { (n)(n), (n)(>n) }

    (build them from example above)

New Racket 1: error

types-match? has an error case: it receives no types and at least one value. So our code needs to throw a Racket error.

We have seen Racket's primitive error function a couple of times. It allows our code to signal Racket-like errors. You can pass to error one of three sets of arguments:

It prints its arguments as part of the error message it produces. For example:
     > (error 'types-match?)
     error: types-match?
     > (error "type mismatch")
     type mismatch
     > (error 'types-match? "must have at least one type")
     types-match?: must have at least one type

If you want to read more about the function, check out the Racket documentation.

Exercise types-match?, Part 2

Write the structurally recursive function named (types-match? types values), where

types-match? should return true if every member of values satisfies the type predicate at the corresponding position in types, and false otherwise. If there are fewer types than values, the function uses the last predicate in types to validate all of the remaining arguments.

For example:

     > (define type-signature (list number? string?))
     > (types-match? type-signature '(42 "Hello, Eugene"))
     > (types-match? type-signature '(42 'eugene))
     > (types-match? type-signature '(42 "Hello" "Eugene"))
     > (types-match? type-signature '(42 "Hello" 'eugene))
     > (types-match? '() '())

You may use higher-order functions such as map wherever they are appropriate. You may find the function every? useful. We wrote this predicate for Homework 4.

Implementing types-match?

interface for special cases
- I use this opportunity to pass (first types) (rest types) to worker
- now, I can watch for empty types and have the last type handy!
- check for error case, too

helper function that is structurally recursive
- the base case maps the last type type over the remaining values
- the pair case "and" recursively

how do we test the error case?  more later...

New Racket 2: Functions with at Least One Argument

  • In Session 6, we learned about variable arity, which indicates that a function can take zero or more arguments. We saw that the syntax to define such a function is
         (lambda args
    When this function is called, args is a parameter bound to a list that contains all the arguments passed to the function.

    Our next exercise asks you to write a new kind of variable-arity function. Sometimes, we want the function to take one or more specific arguments and then allow zero or more additional arguments. We will give specific names to the specific arguments and then gather any remaining arguments into a list.

    For example, suppose that I wanted to define a function named foo that takes one function followed by any number of other arguments:

         (foo + 1 2 3 4 ...)

    We can do this using another variation on lambda's first argument:

         (define foo
           (lambda (f . args)

    This parameter signature says to bind f to the first argument passed to the function and to bind args to a list containing the rest of the arguments. In our example, f will be bound to the value of +, and args will be bound to the list (1 2 3 4 ...).

    You can list any number of names before the ., and each name will be bound to the corresponding argument, with all remaining args put in a list and bound to the name after the dot.

    (Yes, that's the same dot notation we learned about in Session 4... Can you figure out how it makes sense here?)

    Exercise type-checked

    With the types-match? function in hand, we can now create Racket functions that check the types of their arguments before attempting to evaluate.

    Write a function named type-checked that takes as arguments a function f and zero or more type predicates, t1?, t2?, ....

    type-checked returns a variable-arity function that works like this:

    For example:

         > ((type-checked sqrt number?) 100)
         > (define safe-sqrt (type-checked sqrt number?))
         > (safe-sqrt 144)
         > (safe-sqrt "144")
         type-checked: type mismatch ("144")
         > (define safe+ (type-checked + number?))
         > (safe+ 1 2 3 4 5)
         > (safe+ 1 2 'a 4 5)
         type-checked: type mismatch (1 2 a 4 5)
         > (define safe-map (type-checked map procedure? list?))
         > (safe-map sqrt '(1 4 9 16))
         '(1 2 3 4)
         > (safe-map 'sqrt '(1 4 9 16))                 ; that's a symbol!
         type-checked: type mismatch: (sqrt (1 4 9 16))

    type-checked is not recursive. It is simply a higher-order function that uses a recursive function you have already written, types-match?.

    You may generate a more helpful error message if you like. There is room for improvement!

    Implementing type-checked

    Let's take this step by step. type-checked takes as arguments a function f and zero or more type predicates:

        (define type-checked
          (lambda (f . types)

    The value of type-checked is a variable-arity function:

        (define type-checked
          (lambda (f . types)
            (lambda args

    When the returned function is called, if its arguments match the types given to type-checked, then it returns the value of f(x1, x2, ...):

        (define type-checked
          (lambda (f. types)
            (lambda args
              (if (types-match? types args)
                  (apply f args)

    Otherwise, it reports an error. So:

        (define type-checked
          (lambda (f. types)
            (lambda args
              (if (types-match? types args)
                  (apply f args)
                  (error 'type-checked "type mismatch: ~a" args)))))

    Baby steps take us to a solution. Notice: type-checked doesn't do any work to check the types of any arguments; it can't, because it doesn't receive those arguments. It returns function a function that does receive arguments and that uses types-match? to check them.

    The functions type-checked and types-match? show that type checking can be thought of as a decoration on a function definition. With a little bit more work, we could create a new special form named, say, lambda-typed, that lets programmers define types for their formal parameters in much the same way as we do in any statically-typed language. We could even have this special form apply the type checking at definition time, rather than run time, in order to provide the static typing that you all know and love.

    Using a simple language that allows user-defined extensions is a great way for us to see how the underlying mechanics of our more complex languages work: we implement the mechanics in our own language!

    Testing Failure

    In both of our functions today, we have a new kind of situation to deal with. Sometimes, a function must generate an error. How do we test to see that our function causes an error that we expect?

    Consider the cases we saw above:

         > (define safe-sqrt (type-checked sqrt number?))
         > (safe-sqrt 144)
         > (safe-sqrt "144")
         type-checked: type mismatch ("144")

    We have been using rackunit to write unit tests that check to see if our code returns the correct answer. It is easy to check our first safe-sqrt case using check-equal?:

        (check-equal? ((type-checked sqrt number?) 144)

    We need something more powerful than check-equal? for the second case. First of all, there is no value for us to compare to (safe-sqrt "144"). Second, even if there were, our application of safe-sqrt will cause an error before we can do the comparison, interrupting the check! Our test case needs to indicate that the case fails if it doesn't cause an error!

    rackunit provides another primitive to do just that, called check-exn. Here is how we might use it to test safe-sqrt:

        (check-exn exn:fail?
                   (lambda () (safe-sqrt "100")))

    exn:fail? is the name of the kind of error that the error function throws. We wrap our call to safe-sqrt in a lambda, so that Racket can control when it gets called and capture the result. 0-argument lambda expressions of this kind are used to delay computation in many situations, so much so that the idea has its own name: thunk.

    It turns out that rackunit offers us a lot more than equality testing. For example, it also allows us to give a string as the last argument to any check, in order to customize the message it displays when a test fails. I use optional string arguments such as "handles one-argument function" in the tests for today's code. If you would like to learn more, check out the RackUnit API in the on-line documentation, or the documentation in your Racket application.


    1. The image that accompanies the opening section today is courtesy of former UNI CS student David Schmüdde, who is now an artist and programmer in New York City. He took this photo while living in Germany last year for a fellowship. (Link to his original tweet will follow.)

    Wrap Up

    Eugene Wallingford ..... ..... February 22, 2018