An introduction to objects

We mentioned in our introduction that Grace is an Object-Oriented programming language, so it makes sense that we talk about what an object is! Objects let us encapsulate defs, vars, and methods within a single structure.

We make an object by wrapping some declarations with object { and }. This is called an “object constructor”. Here is an object representing a cat:

def culver = object {  // make a new object
    def name = "Culver"
    var miceEaten := 0
    method eatMouse {
        miceEaten := miceEaten + 1
    }
    method miceConsumption {
        miceEaten
    }
    method greeting {
        "{name} says meow"
    }
}

This cat has a name, which cannot be changed (since it is a def), and miceEaten, a variable to keep track of how many mice she has eaten. Furthermore, she has a method eatMouse that increments miceEaten by one each time it is executed. To request execution of a method inside of an object, we use the syntax «receiver».«methodName», where «receiver» is an expression that evaluates to an object, and «methodName» is the name of a method declared on that object. For instance, culver.eatMouse requests that the object culver execute the eatMouse method inside it. Requesting the method miceConsumption will return the current value of miceEaten, and the method greeting will return a suitable string.

What does “encapsulate” mean? Essentially, it means to “draw a box around”. These boxes help to limit the amount of stuff we have to keep in our heads at one time, and also limit the effect of any changes that we make to the code in the box. The box groups the data in the defs and vars with the methods that manipulate it. By default, the methods alone are visible from outside the box.

Because of this encapsulation, that there is no way to access the variable miceEaten other than through the two methods eatMouse and miceConsumption. And there is no way to access the cat’s name at all — although it is used to vary the response from greeting.

Below is another example of a cat object, with a few more bells and whistles.

Statements in a Method

In addition to vars and defs (collectively called fields), an object can contain statements at the “top-level” — that is, directly inside the object constructor. These statements are executed when the constructor is executed. Any expressions used to initialize the fields are executed at the same time, in the order written, from top to bottom.

Making fields visible

Sometime you actually want a field to be visible from the outside.
For example, it would be quite reasonable for culver’s name to be visible. One way to achieve this is to define a method:

def culver = object {  // make a new object
    def myName = "Culver"
    method name { name' }
    ...
    
}

Notice that we have changed the name of the field to myName, and declared a method that will return its value. The method name is visible from outside the object, while myName is visible only inside the object.

There is a shorter way of achieving the same end:

def culver = object {  // make a new object
    def name is public = "Culver"
    ...

}

Adding the annotation is public after the declaration of name makes it visible from the outside, just as if name were a method. Indeed, from the outside, it is impossible to tell a public field from a method. If you annotate the declaration of var thing with is public, the object effectively acquires two additional methods, thing and thing:=(_); the first is called a reader method, and returns the current value of the var, while the second is called a writer method, and changes the value of the var.

Creating several similar objects

If you want to make several objects, for example, several cats, then you can wrap your object constructor in a method declaration. The object can then use the parameters of the method to set its fields. This is quite a common pattern: read about it in the page on classes.