CMSC 15100 — Racket beyond CS 151

Now that you have a significant amount of experience programming with Racket, you may be interested in continuing to program with Racket beyond CS 151 (some of you have already started doing this). We encourage you to keep exploring and working with it—Racket is a great Lisp-descended language that’s very accessible, has great tools, is actively supported, and has a friendly community.

But since you’ll be using Racket for yourself and not for CS 151, you don’t have to (require "../include/cs151-core.rkt") anymore. This gives you full access to Racket’s functions that we’ve disabled for learning purposes. However, there are also a few other differences between the regular language and our slightly modified version of it that you should be aware of.

Structures

What will likely give you the most problems if you were to run your current code without cs151-core are the changes to structures.

  1. The syntax for defining polymorphic structures using define-struct in CS 151 is different.
  2. Structures in CS 151 are “transparent” by default.

The first change is particularly important because it will cause your code to be syntactically incorrect and it will not compile. The second change may affect the behaviour of your program and will cause issues with tests. I’ll describe the changes in more detail and how to “fix” them.

Polymorphic Structures outside of CS 151

In cs151-core, we unified the syntax for define-struct that use type variables to match that of functions. For example, our polymorphic binary tree definition looked like the following.

(define-type (Tree a) (U 'Empty (Node a)))
(define-struct (Node a)
  ([val : a]
   [lsub : (Tree a)]
   [rsub : (Tree a)]))

However, in non-CS 151 Typed Racket, the definition would look like the following:

(define-type (Tree a) (U 'Empty (Node a)))
(define-struct (a) Node
  ([val : a]
   [lsub : (Tree a)]
   [rsub : (Tree a)]))

Notice that the order in which the type variables and the name of the structure Node appear in the definition and that it is different from the type definition above.

In other words, whereever you would have typed

(define-struct (StructName a b) ...)

where a b are type variables, you now need to use

(define-struct (a b) StructName ...)

In some ways, this seems a bit strange since it is different from the define-type syntax. Instead, this mirrors the All type constructor syntax for denoting type variables.

Transparent Structures outside of CS 151

In cs151-core, we made structures “transparent” by default. What does this mean? It’s complicated and there are good reasons for this not to be the default. In practice, this gives other functions less access to the fields of the structure. For example, suppose we have our Point structure.

(define-struct Point
  ([x : Real]
   [y : Real]))

The two main consequences of opaque structures are demonstrated by the following results in the interactions:

> (Point 2 3)
- : Pair
#<Pair>
> (equal? (Pair 2 3) (Pair 2 3))
- : Boolean
#f

Notice that in the first case, the values of the fields of the Point are no longer printed in the interactions window and we only know that we have a Point of some kind. In the second case, notice that equal? (which we’re allowed to use now outside of CS 151) can’t tell that the two Pairs that we gave it are the “same”.

For the purposes of CS 151, it is preferable to have transparent structures because seeing the values of fields in structures in the interactions window is useful for learning and because check-expect would not work otherwise. The second one is particularly problematic if, for example, you forgot to make your structures transparent, so we decided to do it for you by default in cs151-core.

To make structures transparent like they are in CS 151, simply add #:transparent to your structure definition after the definition of the fields:

(define-struct Point
  ([x : Real]
   [y : Real])
  #:transparent)

Images and Universe

The images and universe libraries are not built in to Racket, but they’re not CS 151 inventions either. These were developed to be used with the How to Design Programs book. To use these packages in your code without cs151-core, include some or all of the following lines in your program.

(require typed/2htdp/image)
(require typed/2htdp/universe)

Built-in higher order functions

In CS 151, we streamlined many of the built-in higher-order functions to their basic definitions and functionality. You will find that map, filter, foldr, and foldl allow you to do a bit more than their standard definitions. For example, map can take two or more lists along with an appropriate function and map them onto one list:

> (map + '(1 2 3 4 5) '(2 4 6 8 10))
- : (Listof Positive-Index)
'(3 6 9 12 15)

Of course, this means that map has a more complicated type than we discussed.

> map
- : (All (c a b ...)
      (case->
       (-> (-> a c) (Pairof a (Listof a)) (Pairof c (Listof c)))
       (-> (-> a b ... b c) (Listof a) (Listof b) ... b (Listof c))))
#<procedure:map>

You will find similarly interesting types for filter, foldr, and foldl. You can read more about all of these functions’ behaviour in the Racket documentation.

cons

In CS 151, we defined cons as a function that creates lists. However, the standard definition of cons in Racket, and more generally in Lisp, is that cons creates pairs. This is something to be aware of since if you encounter Lisp or Racket code out in the wilder world, this idea of cons as something that creates pairs is fairly fundamental and is used a lot.

To see this, we can investigate the type for cons.

> cons
- : (All (a b) (case-> (-> a (Listof a) (Listof a)) (-> a b (Pairof a b))))
#<procedure:cons>

The first case in this type, (-> a (Listof a) (Listof a)) is what we’re familiar with. The second one is the one that gives us pairs: (-> a b (Pairof a b)).

This has to do with the definition of a list. Our definition of a list was presented as more structure-based, mirroring that of trees. The more Lisp-y definition of lists is: a list is either

Of course, this is still quite natural for us: we already think of lists as being comprised of a head and tail when we’re processing them.

The new thing you’re allowed to do is create pairs that aren’t lists by using cons. For example,

> (cons 2 "a")
- : (Pairof Positive-Byte String)
'(2 . "a")

gives us the strange looking result '(2 . "a"), which is Lisp’s notation for pairs. Here, Pairof is Racket's built-in type for pairs. Note that this type does not work like our in-class polymorphic structure definition of Pairof. This is similar to the situation where Racket's built-in lists don't work in quite the same way as our UnaryTree. This makes sense, because, as we mentioned earlier, Racket lists are built using Racket pairs.

Note that if we tried writing (cons 2 "a") with cs151-core, we would have encountered a type error instead, because cs151-core redefines cons to have only the type

(All (a) (-> a (Listof a) (Listof a)))

Ordinary Racket

The final thing that you may be interested in is using the ordinary version of Racket, rather than the typed variant. This is a good choice if you’re interested in mainstream Racket programming and it’s what most people use so there will be a lot of resources out there. The Racket website is a great starting point for finding documentation, resources, packages, and where to find others.

To use ordinary Racket, instead of having the line #lang typed/racket at the top of your file, you should use the line #lang racket instead. If you want to use any of the packages we’ve been using, like typed/test-engine/racket-tests or typed/2htdp/image, just leave off the typed/. (Also notice that #lang seems to imply that there are languages other than racket or even typed/racket that can be used with Racket…)

One of the things that you will notice very quickly after your first taste of freedom from the tyranny of the type checker is that the types are still there and they still matter. The difference is that instead of being forced to deal with them before you run your program, if anything goes wrong, you will have to deal with them after you run your program. In other words, you will still have to think about types, but it’ll now be your job to make sure that everything works out.

For instance, suppose we want to define a binary tree. Our tree structure and operations might look like

(define-struct Node (val lsub rsub))

;; tree-contains? determines if the value k is in the tree t
(define (tree-contains? t k)
  (match t
    ['Empty #f]
    [(Node val left right)
     (or (= k val)
         (tree-contains? left k)
         (tree-contains? right k))]))

Notice that since we don’t need to define types,

  1. tree-contains? does not need a type ascription.
  2. The fields of Node do not need type ascriptions and are just listed.
  3. We don’t need to define a type for trees.
  4. We don’t need to worry about polymorphism.

However, the definitions above might be a bit alarming because we don’t really have any context for what a Node or 'Empty are—they kind of just show up. So it turns out that even though we don’t need to provide type information to satisfy the type checker, we still need that information. In this new world without type checking, one thing we can do is turn our type ascriptions into informal contracts specified in comments to the reader.

;; A binary tree (Tree) is either
;; - 'Empty, or
;; - a (Node Integer Tree Tree)
(define-struct Node (val lsub rsub))

;; tree-contains? determines if the value k is in the tree t
;; tree-contains? : Tree Integer -> Boolean
(define (tree-contains? t k)
  (match t
    ['Empty #f]
    [(Node val left right)
     (or (= k val)
         (tree-contains? left k)
         (tree-contains? right k))]))

You might wonder if people really put contracts like this in the comments of Racket code. The answer is maybe, but they definitely put it in the documentation. You are likely familiar with at least one example of this: the official Racket documentation. Note that the Racket documentation you’ve been using all quarter was written for ordinary Racket but still provides exhaustive information about the types for each function.

Of course, unless you’re writing a serious software package, you’re unlikely to write full blown documentation for it. That type information still needs to go somewhere though, so it’s important to at least document this stuff in your code as comments even if you are just writing for yourself.