On this page:
3.1 Introduction
3.2 Assignment
3.2.1 Part 1:   Objects
3.2.1.1 Internal Keywords
3.2.1.2 Advice on Object Implementation
3.2.1.3 How to Use Structs
3.2.1.4 Example
3.2.1.5 Starter Code
3.2.2 Part 2:   Classes
3.2.2.1 Starter Code
3.2.3 Part 3:   Question
3.3 What to Hand In
3 OMac

    3.1 Introduction

    3.2 Assignment

      3.2.1 Part 1: Objects

        3.2.1.1 Internal Keywords

        3.2.1.2 Advice on Object Implementation

        3.2.1.3 How to Use Structs

        3.2.1.4 Example

        3.2.1.5 Starter Code

      3.2.2 Part 2: Classes

        3.2.2.1 Starter Code

      3.2.3 Part 3: Question

    3.3 What to Hand In

3.1 Introduction

In the Macros assignment, we learned how to implement Racket macros through a series of toy exercises. In this assignment, we’ll build a more complex and interesting series of object systems using Racket macros yet again (hence the name “OMac”).

3.2 Assignment

This assignment consists of three parts: defining objects, defining classes, and a short analysis question. All code for this assignment will be written in #lang racket (though you are welcome to use the SMoL compatibility layers).

3.2.1 Part 1: Objects

You will define two macros in this assignment:
  • object, representing an object containing fields and methods

    (object (fields [fn iv] ...)

            (methods [mn imp] ...))

    where fn is a field name (symbol), mn is a method name (also a symbol), and iv (“initial value”) and imp (“implementation”) are both expressions.
    All methods are explicitly declared as functions (typically defined using a lambda in place, but the function could also be defined outside and referenced from inside the object).
    Also, all methods must take at least one argument. This argument will be the object itself. Conventionally, this argument would be called self or this (but it doesn’t have to be). The call macro is responsible for passing the object along as the first parameter.

There are generally two schools of thought on handling the self argument. In Java, the name this is bound automatically, and does not show up in the headers. In contrast, in Python, the name is not chosen by the language, and the programmer explicitly chooses it. We could design our object macro to automatically bind a name, too. There are multiple ways of doing it in Racket using features slightly more advanced than what we’ve used in this course. The latter design arguably has less “magic”. However, it means that a method declaration always has one more field than a corresponding call to the same method. Relatedly, it may actually be more confusing to students.

  • call, used to call methods on an object:

    (call o mn args ...)

    where o is an object, mn is a method name (a symbol), and args ... are arguments to that method (all expressions).
    If o does not have a method by the name of mn, you should raise an error. Our stencil provides a function raise-method-not-found-exception for doing this, which you should use by passing raise-method-not-found-exception the invalid method name. Missing field errors should be handled directly by Racket.
    Note: call’s specification enforces that only methods are accessible from outside an object. Fields are effectively private, and can only be referenced by methods from inside the object.

3.2.1.1 Internal Keywords

If you haven’t already, you will need to learn about “internal keywords”: the purpose of the mysterious () that appears in syntax-rules. Inside these parentheses you can name symbols that have to appear in the precisely named places in the input. This helps you write a macro that expects the keywords fields and methods.
Consider, for instance, the following macro:
#lang racket
(define-syntax foo
  (syntax-rules (bar baz)
    [(_ [bar x ...] [baz y ...])
     (begin
       (println "all the bars")
       (println x) ...
       (println "all the bazs")
       (println y) ...)]))
Replacing (bar baz) in line 2 with () makes bar and baz regular pattern variables and they just match against anything that is in that position. However, when bar and baz are included in the internal keywords list, those words must explicitly appear in order for the pattern to be matched against. Thus:
#lang racket
(foo [bar 1 2] [baz 3])
(foo [bar] [baz])
are both legitimate uses of the macro foo, but:
#lang racket
(foo [baz 3])
(foo [bar])
(foo [1 2] [3 4 5])
all result in syntax errors because they don’t match any of the macro’s patterns as they do not contain the words bar and baz in the expected places.
You can check out the racket documentation for more information.

3.2.1.2 Advice on Object Implementation

There are several ways to represent objects internally. We recommend that you create a simple struct that stands for objects. A struct in Racket is a way to define a new type. See the next section for details of how to work with structs.

It might help to think of the object form as being roughly analogous to a JavaScript literal object (JSON).

A key advantage to this approach: when you get to classes, you may accidentally pass classes where you intended objects or vice versa. If the two have a similar representation, then you could get very confusing output. In contrast, having explicit structures catches confusion early, even if your class implementation has a similar representation to your object representation.
Be careful when implementing a complex construct like this. It’s possible to create an implementation that works most of the time but is subtly wrong (as in, does something undesirable). Think about how features can interact.

3.2.1.3 How to Use Structs

The basic syntax for making a struct type is:

(struct struct-id (field-id ...))

For instance, to define a card type with suit and value, you would do:

(struct card (suit value))

Once you have a struct defined, you can create an instance with struct-id:

(struct-id field-val ...)

In our card example, we could create an instance of a card like so:

(define four-of-hearts (card "hearts" 4))

We also get a struct-id? predicate, which will tell us if something is an instance of our struct:

(card? four-of-hearts) ; #t

(card? "hello") ; #f

Finally, to access the fields of a struct, we use an accessor struct-id-field-id:

(card-suit four-of-hearts) ; "hearts"

(card-value four-of-hearts) ; 4

Structs have many more advanced features, but you won’t need anything beyond these ones for this assignment.

3.2.1.4 Example
#lang racket
(define cowboy-object
  (object
   (fields [name "Timmy the Cowboy"])
   (methods
    [say-howdy-to
     (lambda (self to)
       (string-append name " says: Howdy " to "!"))]
    [get-name
     (lambda (self) name)]
    [set-name
     (lambda (self x) (set! name x))])))
 
(call cowboy-object say-howdy-to "Partner") ; returns "Timmy the Cowboy says: Howdy Partner!"
3.2.1.5 Starter Code

We’ve provided starter code for Part 1 at objects.rkt. You are free to add any other definitions to this file that you need for your implementation (for example, a struct). However, do not edit the contents of the file above the “do not edit” line; if you do, you will get a zero on this part of the assignment.
We’ve also provided a stencil for your test cases at objects-tests.rkt and testing support code at test-support.rkt.

Please read the Testing Guidelines for guidelines on how to write tests for the Implementation assignments.

3.2.2 Part 2: Classes

In this part, we’ll upgrade from objects to classes.
You will define three macros: class, used to define a new class; call, used to call methods on an class instance; and new, which creates a new instance of the given class-name.
#lang racket
(class class-name
  (fields [fn iv] ...)
  (methods [mn imp] ...))
 
(new class-name)
where a class-name is represented as a symbol.
Fields and methods are just as before.
Thus, the following code should create an object that is effectively equivalent to the one in Example:
#lang racket
(class Cowboy
  (fields [name "Timmy the Cowboy"])
  (methods [say-howdy-to
            (lambda (self to)
              (string-append name " says: Howdy " to "!"))]
           [get-name
            (lambda (self) name)]
           [set-name
            (lambda (self x) (set! name x))]))
 
(new Cowboy)
You are welcome to use your object macro when defining classes, but are not required to. As discussed earlier, we recommend creating a struct to represent classes.

3.2.2.1 Starter Code

We’ve provided starter code for Part 2 at classes.rkt. As in Part 1, you are free to add any other definitions to this file that you need for your implementation.

If you are using your object macro in Part 2: do not import your object macro directly from objects.rkt. Instead, you should copy your object definition into your classes.rkt file. This is necessary for autograding purposes.

We’ve also provided a stencil for your test cases at classes-tests.rkt. This file imports from the test-support.rkt given in Part 1: Objects, so make sure you have that before working on your tests.

3.2.3 Part 3: Question

Finally, give a concise, illustrative response to the following question:
Can you see why we asked you to program in Racket rather than Plait? What would go wrong if you used Plait instead?
Hint: Try a small experiment (you can copy most of your code over) in Plait and see for yourself. (The answer is not something simple like a missing library, but rather a bit deeper. The simplest test may not reveal it. Remember that the primary difference between the two lies in Plait’s type system.)

3.3 What to Hand In

You will submit four files for this assignment:
  • objects.rkt, which should be uploaded to the “Objects - Code” drop on Gradescope.

  • classes.rkt, which should be uploaded to the “Classes - Code” drop on Gradescope.

  • objects-tests.rkt, which should be uploaded to the “Objects - Tests” drop on Gradescope.

  • classes-tests.rkt, which should be uploaded to the “Classes - Tests” drop on Gradescope.

Finally, submit your answer to Part 3: Question to the “Question” drop on Gradescope.
You can update your submissions as many times as you want before the deadline.