11. Class & Specifiers

Class

In Verse, a class is a template for creating objects with similar behaviors and properties. It is a composite type, which means that it’s bundled data of other types and functions that can operate on that data.

For example, let’s say you want to have multiple cats in your game. A cat has a name and an age, and they can meow. Your cat class could look like this:

cat := class:
    Name : string
    var Age : int = 0
    Sound : string
 
    Meow() : void = DisplayMessage(Sound)
  • Definitions of variables that are nested inside the class define fields of the class. Functions defined inside a class may also be called methods. Functions and methods are referred to as class members. In the above example, Sound is a field, and Meow is a method of cat.

  • Fields of a class may or may not have a default value, or may only define a type that limits the values that the field may have. In the above example, Name does not have a default value, while Age does. The value may be specified using an expression that has the effects. The default value expression may not use the identifier Self, which you'll learn about below.

For instance, lets say you want your cats to be able to tilt their heads. You can initialize an initial rotation HeadTilt in the following code using the IdentityRotation() method because it has the <converges> specifier and is guaranteed to complete with no side effects.

cat := class:
    ...
    # A valid expression
    HeadTilt:rotation = IdentityRotation()

Constructing a Class

  • With a class that defines what a cat is and what the cat can do, you can construct an instance of the class from an archetype. An archetype defines the values of the class fields.

For example, let’s make an old cat named Percy from the cat class:

OldCat := cat{Name := ”Percy”, Age := 20, Sound:= ”Rrrr”}

In this example, the archetype is the part between { and }. It doesn't need to define values for all fields of the class, but must at least define values for all fields that don't have a default value. If any field is omitted, then the instance constructed will have the default value for that field.

In this case, the cat class Age field has a default value assigned to it of (0). Since the field has a default value, you’re not required to provide a value for it when constructing an instance of the class. The field is a variable, which means that though you might provide a value at construction time, the value of that variable can be changed after construction.

By contrast, the Name field of cat is not a mutable variable, and so is immutable by default. This means that you can provide a default value for it at construction time, but after construction, it cannot change: it is immutable.

Since a class in Verse is a template, you can make as many instances as you want from the cat class. Let’s make a kitten named Flash:

Kitten := cat{Name := ”Flash”, Age := 1, Sound := ”Mew”}

Accessing Fields

Now that you have some instances of cat, you can access each cat’s Name field with OldCat.Name or Kitten.Name, and call each cat's Meow method with OldCat.Meow() or Kitten.Meow().

Both cats have the same named fields, but those fields have different values. For example, OldCat.Meow() and Kitten.Meow() behave differently because their Sound fields have different values.

Self

  • Self is a special identifier in Verse that can be used in a class method to refer to the instance of the class that the method was called on. You can refer to other fields of the instance the method was called on without using Self, but if you want to refer to the instance as a whole, you must use Self.

For example, if DisplayMessage required an argument for which pet to associate a message with:

DisplayMessage(Pet:pet, Message:string) : void = …
cat := class:

    Meow() : void = DisplayMessage(Self, Sound)

If you wanted to initialize a louder version of your cats meow, you might think you could build off Sound variable you already set up. This will not work in the following code however, because LoudSound cannot reference the instance member Sound, since default value expressions cant use the identifier Self

cat := class:
    ...
    Sound : string
    Meow() : void = DisplayMessage(Self, Sound)
    # The following will fail since default class values
    # can't reference Self
    LoudSound : string = "Loud " + Self.Sound 
    LoudMeow() : void = DisplayMessage(Self, LoudSound)

Subclasses/Inheritance

Classes can inherit from a superclass, which includes all fields of the superclass in the inheriting class. Such classes are said to be a subclass of the superclass. For example:

 Some pet := class:
    Name : string
    var Age : int = 0
 
cat := class(pet):
    Sound : string
    Meow() : void = DisplayMessage(Self, Sound)
 
dog := class(pet):
    Trick : string
    DoTrick() : void = DisplayMessage(Self, Trick)
code

Here, the use of class(pet) when defining cat and dog declares that they inherit from the pet class. In other words, they are subclasses of pet.

This has several advantages:

  1. Since both cats and dogs have names and ages, those fields only have to be defined once, in the pet class. Both the cat and the dog field will inherit those fields.

  2. The pet class can be used as a type to refer to instance of any subclass of pet. For example, if you want to write a function that just needs the name of a pet, you can write the function once for both cats and dogs, and any other pet subclasses you might introduce in the future:

IncreaseAge(Pet : pet) : void=
 set Pet.Age += 1

Overrides

When you define a subclass, you can override fields defined in the superclass to make their type more specific, or change their default value. To do so, you must write the definition of the field in your subclass again, but with the <override> specifier on its name. For example, you can add a Lives field to pet with a default value of 1, and override the default value for cats to be 9:

pet := class:

    Lives : int = 1
 
cat := class(pet):

    Lives<override> : int = 9

Method Calls

When you access a field of a class instance, you access that instance's value for the field. For methods, the field is a function, and overriding it replaces the field's value with a new function. Calling a method calls the value of the field. This means that the method called is determined by the instance. Consider the follow example:

pet := class:

    OnHearName() : void = {}
 
cat := class(pet):

    OnHearName<override>() : void = Meow()
 
dog := class(pet):

    OnHearName<override>() : void = DoTrick()
 
CallFor(Pet:pet):void=
    DisplayMessage("Yoo hoo {Pet.Name}!")
    Pet.OnHearName()

If you write CallFor(Percy), it will call the OnHearName method as defined by cat. If you write CallFor(Fido) where Fido is an instance of the dog class, then it will call the OnHearName method as defined by dog.

Interfaces

Interfaces are a limited form of classes that can only contain methods that don't have a value. Classes can only inherit from a single other class, but can inherit from any number of interfaces.

Persistable Type

A class is persistable when:

  • Defined with the persistable specifier.

  • Defined with the final specifier, because persistable classes cannot have subclasses.

  • Not unique.

  • Does not have a superclass.

  • Not parametric.

  • Only contains members that are also persistable.

  • Does not have variable members.

When a class is persistable, it means that you can use them in your module-scoped weak_map variables and have their values persist across game sessions. For more details on persistence in Verse, check out Using Persistable Data in Verse.

The following Verse example shows how you can define a custom player profile in a class that can be stored, updated, and accessed later for a player. The class player_profile_data stores information for a player, such as their earned XP, their rank, and quests they’ve completed.

player_profile_data := class<final><persistable>:
    Version:int = 1
    Class:player_class = player_class.Villager
    XP:int = 0
    Rank:int = 0
    CompletedQuestCount:int = 0
    QuestHistory:[]string = array{}

Block Expressions

You can use block expressions in a class body. When you create an instance of the class, the block expressions are executed in the order they are defined. Functions called in block expressions in the class body cannot have the NoRollback effect.

As an example, let’s add two block expressions to the cat class body and add the transacts effect specifier to the Meow() method because the default effect for methods has the NoRollback effect.

cat := class():
    Name : string
    Age : int
    Sound : string
 
    Meow()<transacts> : void =
        DisplayOnScreen(Sound)
 
    block:
	    Self.Meow()
 
    block:
        Log(Self.Name)
 
OldCat := cat{Name := "Garfield", Age := 20, Sound := "Rrrr"}

When the instance of the cat class, OldCat, is created, the two block expressions are executed: the cat will first say “Rrrr”; then “Garfield” will print to the output log.

Specifiers

Visibility Specifiers

You can add visibility specifiers to class fields and methods to control who has access to them. For example, you can add the private specifier to the Sound field so only the owning class can access that private field.

cat := class:

    Sound<private> : string
 
MrSnuffles := cat{Sound := "Purr"}
MrSnuffles.Sound # Error: cannot access a private field

The following are all the visibility specifiers you can use with classes:

  • public: Unrestricted access.

  • internal: Access limited to current module. This is the default visibility.

  • protected: Access limited to current class and any subclasses.

  • private: Access limited to current class.

Access Specifiers

You can add access specifiers to a class to control who can construct them. This is useful, for example, if you want to make sure an instance of a class can only be constructed at a certain scope.

pets := module:
    cat<public> := class<internal>:
        Sound<public> : string = "Meow"
 
GetCatSound(InCat:pets.cat):string =
    return InCat.Sound # Valid: References the cat class but does not call its constructor
 
MakeCat():void =
    MyNewCat := pets.cat{} # Error: Invalid access of internal class constructor

Calling the constructor for the cat class outside of its module pets will fail because the class keyword is marked as internal. This is true even though the class identifier itself is marked as public, which means cat can be referenced by code outside the pets module.

The following are all the access specifiers you can use with the class keyword:

  • public: Unrestricted access. This is the default access.

  • internal: Access limited to the current module.

Concrete Specifier

When a class has the concrete specifier, it is possible to construct it with an empty archetype, such as cat{}. This means that every field of the class must have a default value. Furthermore, every subclass of a concrete class must itself be concrete.

For example:

class1 := class<concrete>:
    Property : int = 0
 
# Error: Property isn't initialized
class2 := class<concrete>:
    Property : int
 
# Error: class3 must also have the <concrete> specifier since it inherits from class1
class3 := class(class1):
    Property : int = 0

A concrete class can only inherit directly from an abstract class if both classes are defined in the same module. However, it does not hold transitively — a concrete class can inherit directly from a second concrete class in another module where that second concrete class inherits directly from an abstract class in its module.

Unique Specifier

The unique specifier can be applied to a class to make it a unique class. To construct an instance of a unique class, Verse allocates a unique identity for the resulting instance. This allows instances of unique classes to be compared for equality by comparing their identities. Classes without the unique specifier don't have any such identity, and so can only be compared for equality based on the values of their fields.

This means that unique classes can be compared with the = and <> operators, and are subtypes of the comparable type.

For example:

unique_class := class<unique>:
    Field : int
 
Main()<decides> : void =
    X := unique_class{Field := 1}
    X = X # X is equal to itself
    Y := unique_class{Field := 1}
    X <> Y # X and Y are unique and therefore not equal

Final Specifier

You can only use the final specifier on classes and fields of classes.

When a class has the final specifier, you cannot create a subclass of the class. In the following example, you cannot use the pet class as a superclass, because the class has the final specifier.

pet := class<final>():

 
cat := class(pet): # Error: cannot subclass a “final” class

When a field has the final specifier, you cannot override the field in a subclass. In the following example, the cat class can’t override the Owner field, because the field has the final specifier.

pet := class():
    Owner<final> : string = “Andy”
 
cat := class(pet):
    Owner<override> : string = “Sid” # Error: cannot override “final” field

When a method has the final specifier, you cannot override the method in a subclass. In the following example, the cat class can’t override the GetName() method, because the method has the final specifier.

pet := class():
    Name : string
 
    GetName<final>() : string = Name
 
cat := class(pet):

    GetName<override>() : string =  # Error: cannot override “final” method

Last updated

Was this helpful?