Classes
Classes define an object's behavior and state. Behavior is defined by methods which live in the class. Every object of the same class supports the same methods. State is defined in fields, whose values are stored in each instance.
Ghost's class syntax follows JavaScript and TypeScript conventions: methods are declared by name with no function keyword, and instances are created with new.
Defining A Class
Classes are created using the class keyword, unsurprisingly:
class CoffeeMaker {
//
}
This creates a class named CoffeeMaker with no methods or fields.
Methods
To add functionality to our coffee maker class, we need to give it methods. A method is a name, a parameter list, and a body:
class CoffeeMaker {
brew() {
print("Your coffee is now brewing.")
}
}
This defines a brew method that takes no arguments. To add parameters, put their names inside the parentheses:
class CoffeeMaker {
brew(dosage, temperature) {
print("Your %s of coffee is now brewing at %s degrees.".format(dosage, temperature))
}
}
Methods take default parameter values just like functions do.
A method body's scope is the class itself, so a method can call a sibling method by bare name:
class CoffeeMaker {
brew() {
heat()
}
heat() {
print("Heating...")
}
}
Constructors
To create instances of a class, we need a constructor. It is an ordinary method with the reserved name constructor:
class CoffeeMaker {
constructor(grind, temperature) {
print("Grind set to: %s".format(grind))
print("Temperature set to: %s".format(temperature))
}
}
Instances are built with the new keyword:
drip = new CoffeeMaker("flat", "200")
chemex = new CoffeeMaker("coarse", "202")
pourOver = new CoffeeMaker("fine", "202")
frenchPress = new CoffeeMaker("very coarse", "202")
Note that we didn't call the constructor method directly. new creates the instance first, then invokes the constructor on it. That distinction matters, because inside the constructor body you can already use this, assign fields, and call other methods.
Fields
State lives in fields. Each field has a name, is reached through this, and behaves like a variable.
class CoffeeMaker {
constructor(grind, temperature) {
this.grind = grind
this.temperature = temperature
this.printSettings()
}
printSettings() {
print("Grind set to: %s".format(this.grind))
print("Temperature set to: %s".format(this.temperature))
}
}
A field can also be declared directly in the class body, with an initial value:
class Animal {
legs = 4
name = "unnamed"
}
print(new Animal().legs) // >> 4
These declarations are initializers, not shared class state. They are re-evaluated for every instance — ancestors first, then the class itself — before the constructor runs, so two instances never share a field's value.
Method Scope
Up to this point, "scope" has been used to talk exclusively about variables. In a procedural language like C, or a functional one like Scheme, that's the only kind of scope there is. But object-oriented languages like Ghost introduce another kind of scope: object scope. It contains the methods that are available on an object. When you write:
coffee.brew()
you're saying "look up the method brew in the scope of the object coffee". That's what . does, and the object to the left of the period is the object you want to look up the method on.
this
Things get more interesting when you're inside the body of a method. When the method is called on some object and the body is being executed, you often need to access that object itself. You can do that using this.
class CoffeeMaker {
setGrind(grind) {
this.grind = grind
}
printGrind() {
this.setGrind("coarse")
print(this.grind)
}
}
The this keyword works sort of like a variable, but has special behavior. It always refers to the instance whose method is currently being executed. This lets you invoke methods on "yourself".
It's an error to refer to this outside of a method.
class CoffeeMaker {
setGrind(grind) {
this.grind = grind
}
printGrindThrice() {
this.setGrind("coarse")
for (i in 1 .. 3) {
print(this.grind)
}
}
}
This is unlike Lua and Dart which can "forget" this when you create a callback inside a method. Ghost does what you want here and retains the reference to the original object.
(In technical terms, a function's closure includes this. Ghost can do this because it makes a distinction between methods and functions.)
Inheritance
A class can inherit from a "parent" or superclass. When you invoke a method on an object of some class, if it can't be found, it walks up the chain of superclasses looking for it there.
To inherit another class, use extends when you declare your class:
class Bar extends Foo {
//
}
This declares a new class Bar that inherits from Foo.
super
super reaches the version of a member defined by the superclass of the class the running method was declared in. It is how an overriding method calls the one it overrode, and how a subclass constructor runs its parent's:
class Animal {
constructor(name) {
this.name = name
}
speak() {
return "..."
}
}
class Dog extends Animal {
constructor(name) {
super.constructor(name)
}
speak() {
return super.speak() + " woof"
}
}
print(new Dog("Fido").speak()) // >> ... woof
A subclass constructor that doesn't call super.constructor() simply doesn't run the parent's — field declarations from ancestors are still applied either way.
Traits
In addition to class inheritance, Ghost supports traits. Traits are like classes, but they can't be instantiated. Instead, they're used to share methods and fields between classes.
trait Brewable {
brew() {
print("Your coffee is now brewing.")
}
}
This defines a trait named Brewable with a brew method. To use it, you use the use keyword inside the class body:
class CoffeeMaker {
use Brewable
}
It's as if you had written:
class CoffeeMaker {
brew() {
print("Your coffee is now brewing.")
}
}
You can use multiple traits by separating them with commas:
class CoffeeMaker {
use Brewable, Cleanable
}
A trait's methods can call methods the using class provides, which is the usual way to build one:
trait Loud {
shout() {
return this.speak().toUpperCase()
}
}
class Dog {
use Loud
speak() {
return "woof"
}
}
print(new Dog().shout()) // >> WOOF
Comparing Instances
Instances compare by identity. Two instances of the same class holding equal fields are still two different objects, so == between them is false unless they are literally the same object.