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 Making symbols from expressions in identifiers
3.2.1.3 Advice on Object Implementation
3.2.1.4 How to Use Structs
3.2.1.5 Example
3.2.1.6 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 Making symbols from expressions in identifiers

        3.2.1.3 Advice on Object Implementation

        3.2.1.4 How to Use Structs

        3.2.1.5 Example

        3.2.1.6 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 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).
    The methods may be evaluated when the object is created or each time the method is called, but all other values (like initial values for fields) should be evaluated immediately.
    You may assume there is no overlap between field names and method names.
    Finally, 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 Making symbols from expressions in identifiers

By adding before a name that’s been bound by a macro, we can convert the corresponding expression into symbol.
#lang racket
(define-syntax make-symbol
  (syntax-rules ()
    [(_ sym) 'sym]))
(make-symbol hi) ; returns 'hi
You may find it useful to keep this in mind as you implement object and call.

3.2.1.3 Advice on Object Implementation

There are several ways to represent objects internally. Whatever representation you decide on, you may want to wrap it in a simple struct to ease debugging. When you get to Part 2: Classes, you may have very similar internal representations for classes and objects. Having classes and objects wrapped in separate structs will help ease debugging in the event that you accidentally pass a class where an object is expected or vice-versa.
A struct in Racket is 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).

Be careful when implementing complex constructs like objects. 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.4 How to Use Structs

The basic syntax for making a struct type is:
#lang racket
(struct <struct-id> (<field-id> ...))
For instance, to define a card type with suit and value, you would do:
#lang racket
(struct card (suit value))
Once you have a struct defined, you can create an instance with <struct-id>:
#lang racket
(<struct-id> <field-val> ...)
In our card example, we could create an instance of a card like so:
#lang racket
(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:
#lang racket
(card? four-of-hearts) ; #t
(card? "hello") ; #f
Finally, to access the fields of a struct, we use an accessor <struct-id>-<field-id>:
#lang racket
(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.5 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.6 Starter Code

We’ve set up a GitHub Classroom assignment that contains all necessary stencil code and support code here.
If you’re working in pairs, first, have one group member accept the assignment and create a team name of your choosing (appropriate, please!). Then, have the second group member select the team so both of you are working in the same repository. If you are working alone, you can just create a team with your own username (or anything) as the team name.
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 wrap your class representation.

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.

You may also select the entire omac repository to submit to both drops on Gradescope. You can update your submissions as many times as you want before the deadline. If you have a partner, you can also add them to your submission 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.