Skip to main content
The Squirrel Programming Guide

Classes And Instances

Contents

Introduction

Squirrel is an object-oriented language. In essence this means that it uses objects — program modules which contain functions (called methods) and data (properties) that work with the main body of the application but can operate separately.

The idea is that programs should be compartmentalized. An object encompasses a certain kind of information relevant to the application and incorporates the code needed to perform actions upon that information — and nothing else. Traditionally, ‘information’ in this context has meant some kind of data record: a single employee in a payroll or staff management program, for instance. But it has come to cover any entity that the program needs to work with.

For example, imagine you are working on two applications both of which run on imp-enabled products make use of the same type of seven-segment LED display. Perhaps one application displays a temperature on the display, the other the current time. Both applications therefore need code that allows them to talk to the display to send it whatever data they need it to present. Rather than write one block of code that displays a temperature on the display, and another block of code that displays the time, it’s more efficient to write just one block of code that can display any numerical data; both applications use that same code but send it different data to display.

This also has the advantage that if you need to use the same display in a third project, you have the code ready to use.

Each of these applications needs a display object that represents the real display. Where do these objects come from? What you actually write when you create the code block mentioned above is a class. This is a kind of template which is used to generate instances of that class. Think of the class as a cookie cutter, and the instances as initially identical cookies punched out with the cutter but which are then iced in different colors. Instances and classes are objects.

The first two products mentioned above have only one display each, so require only one instance if the display driver class apiece, the third product might have two LEDs (one to show a countdown to the completion of a task, the other to display the current time) so it will work with two instances of the display driver class, one per physical display.

Crucially, the object contains all the code needed to work with the display; all the host application needs to know is how to interact with the object. The object doesn’t need to know anything about the host application. In this design model, your application consists of a small amount of code that runs the communication between multiple objects.

Class Basics

Classes are declared using the keyword class and comprise methods and properties respectively defined with functions and variables. Class and instance names may not start with a numeral. It is conventional, but not mandatory, to name classes with an initial capital letter. For example:

class Display {

    // Declare the class properties
    clockPin = null;
    dataPin = null;

    // Declare the class functions
    function displayText(text) {
        . . .
    }
}

Class property declarations do not require the local keyword.

Instances are created (instantiated) by calling the class as if it were a function:

local displayOne = Display();
local displayTwo = Display();

Each instance is unique. Squirrel’s instanceof keyword may be used to test whether the passed object is an instance of a given class.

Note Standard Squirrel allows metadata strings to be added to classes using the </ and /> markers. However, this functionality is not currently supported by impOS.

Properties and methods are accessed using dot syntax:

local displayOne = Display();
displayOne.displayText("Hello, World!");

Constructors

Classes may also contain a special function, called constructor(), which is run when the class is instantiated to supply default property values, some of which may be passed as parameters, and to perform other setup and initialization operations:

class Display {

    // Declare the class properties
    clockPin = null;
    dataPin = null;

    // Declare the constructor
    constructor(cPin, dPin) {
        clockPin = cPin;
        dataPin = dPin;
        displayText("LED Ready");
    }

    // Declare the class functions
    function displayText(text) {
        . . .
    }
}

local displayOne = Display(hardware.pinA, hardware.pinB);
local displayTwo = Display(hardware.pinR, hardware.pinS);

Unique Properties Vs Shared Properties

If a class definition contains a constructor(), class properties of reference type that will set by that function should be assigned to null when they are declared, as in the examples above. This ensures that these properties are unique to the instance and not shared among instances; shared properties should not be declared null but declared with a suitable initializer: for example, [] for an array, or {} for a table.

By convention, property declarations appear before the constructor.

Scalar properties — integers, floats, booleans and strings — are, by contrast, not shared between instances, but if you wish to do so, prefix their declaration with the keyword static. However, this makes them read-only (unlike C/C++ usage). Attempts to change their value will cause an exception to be thrown.

Reference-type variables may also be prefixed with static, in which case all instances will not only share the referenced entity but the referenced entity can’t be changed outside of the class. For example, the following code will fail with an "index 'pins' does not exist" error:

class Peripheral {

    static pins = {};
}

local deviceOne = Peripheral();
deviceOne.pins = {};

However, the properties of the object referred to can be changed within the class:

class Peripheral {

    static pins = {};

    constructor() {
        pins.count <- 4;
    }
}

Squirrel also disallows the modification of properties when they are accessed via the class rather than the instance. For example:

class Peripheral {

    pins = {};
    id = 0;

    function setID(value) {
        if (value > -1 && value < 256) id = value;
    }
}

Peripheral.setID(4);

will fail with a "cannot set property of object of type 'class'" error. It will not fail if the method is called on an instance of the class:

local p = Peripheral();
p.setID(4);

Incidentally, you can work around this if you use a table instead of a class:

local Peripheral = {

    pins = {},
    id = 0,
    setID = function(value) {
        if (value > -1 && value < 256) id = value;
    }
}

Peripheral.setID(4);

This also has the benefit of using less memory: please see ‘Writing Efficient Squirrel’ for more details.

Finally, instances are initialized only once and this governs how properties are shared. If a property is of a reference type, and you initialize it with a literal, then it will be shared. To avoid this, don't initialize such properties with values; assign them to null and use the constructor to set their initial values, whether default literals or values passed in from the host program.

Summary

  • Non-static, scalar property — Can be assigned to; each instance has its own independent copy but the initializer (if any) is only evaluated once.
  • Static, scalar property — Cannot be assigned to; is shared between all instances.
  • Non-static, reference property — Can be assigned to; the referent can be modified; each instance has its own independent reference but the initializer (if any) is only evaluated once, so if the initializer is a literal, then each instance starts out referring to a single shared referent.
  • Static, reference property — Cannot be assigned to; referent can be modified; the instance is shared between all instances.

Expanding Classes

Classes can be expanded on the fly. For example, you can add member functions with the slot operator (<-):

pixels = WS2812(hardware.spi257, 5);

WS2812.len <- function() {
    local size = _bits.len() + _frame.len();
    return size;
};

server.log("Pixels size: " + pixels.len());

If you add a method to the class this way, it will be available not only to new instances of the class, but also all existing instances.

Iterating Through Class Members

You can iterate through a class’ members using a foreach loop:

#require "WS2812.class.nut:3.0.0"

foreach (member, value in WS2812) {
    server.log("Member \'" + member + "\' has value: " + value);
}

or through the members’ values:

#require "WS2812.class.nut:3.0.0"

local i = 1;
foreach (value in WS2812) {
    server.log("Member " + i + " value: " + value);
    i++;
}

Checking Whether A Class Has A Certain Member

The in keyword may be used to determine whether a class contains a specified member:

#require "WS2812.class.nut:3.0.0"

if ("draw" in WS2812) {
    server.log("WS2812 has a draw() method");
}

You can combine this with class expansion:

#require "WS2812.class.nut:3.0.0"

if (!("len" in WS2812)) {
    WS2812.len <- function() {
        local size = _bits.len() + _frame.len();
        return size;
    };
}

Object Inheritance

Squirrel allows you to build classes out of other classes using a property called inheritance. By this means, you can create a general, parent class and then derived, child classes that offer more specific functionality by inheriting certain methods and properties from the parent, or overriding them with more specific implementations.

For example, the Holtek HT16K33 chip is widely used to drive a large number of different LED displays. Some are dot-matrix panels, others seven- or 14-segment alphanumeric displays. To support all of these, you might write a parent class that interfaces with the the HT16K33, and then write child classes that extend the parent with methods specific to the types of physical display the HT16K33 chip is connected to.

A class may be derived from another class by using the extends keyword with the original, parent class name:

class HT16K33Matrix extends HT16K33

The new class inherits all of the methods and properties of the parent class. In addition, it may add its own, unique methods and properties (which are only available to it and its instances). The child may even override some of the inherited methods, ie. replace them with code of its own. While an overriding method will be used if it is called in the usual way, the overridden version may also be called from within the child class by prefixing the method’s name with the base keyword. For example:

class Parent {
    function functionOne() {
        return 42;
    }

    function functionTwo() {
        return "Mega-City One";
    }
}

class Child extends Parent {
    // Child overrides Parent's functionTwo() with its own version of the method
    function functionTwo() {
        return "Brit Cit";
    }

    // Child adds a unique method, functionThree(), not available to the parent,
    // but which makes use of a parent method
    function functionThree() {
        return base.functionTwo();
    }
}

local achild = Child();
local x = achild.functionOne();    // x = 42
local y = achild.functionTwo();    // y = "Brit Cit"
local z = achild.functionThree();  // z = "Mega-City One"

Duplicating Objects

Objects in Squirrel are accessed by reference. Copying a variable that references an object (class or class instance) simply duplicates the reference — it does not duplicate the object itself.

local a = LED();    // 'a' points to an instance of LED()
local b = a;        // 'b' points to the *same* instance of LED()
b.name = "Ted";
server.log(a.name); // This displays 'Ted' because a.name and b.name are the same
a = null;           // 'a' no longer points to the instance
b = LED();          // 'b' now points to a second, new instance of LED

To duplicate objects (and, indeed, other referenced entities, including arrays and tables), Squirrel provides a keyword, clone. It’s important to remember that any such entities nested within the clone’d object will not be copied; it is the references to them that are copied. However, they themselves may be clone’d separately — and, indeed, any objects, tables arrays nested within them — and added to the clone.

What Happens to Unreferenced Objects

In the example above, the first instance of LED is referenced by the variable a and then the variable b, but both of these variables are subsequently given other values: null in the case of a, and b gets a reference to a second LED instance. What happens to the first instance? Squirrel tracks entities that are accessed by reference and when any such entity is no longer referenced by any variable, it is disposed of and the memory it was occupying is freed for use. This process is called ‘garbage collection’.

Variables will stop referencing an object if they are reassigned, or if they go out of scope.

Object Delegation

If an object lacks a certain method, it may delegate a call to that method to another object. An instance’s delegate is always its class and this is how existing instances gain access to methods subsequently added to the class.

Weak References

Classes and instances are accessed by reference. That is, when you assign an object to a variable, the variables holds not the object itself but information that tells Squirrel where the object can be found. As we saw in the introduction to variables, there are two types of reference: strong and weak. When you create an object, Squirrel returns a strong reference to it, and it’s this strong reference that is placed in the variable during assignment:

local a = Display();    // 'a' takes the reference returned by the call to 'Display()'

String references are generally preferred because they bind the variable to the object so that Squirrel can’t dispose of the object — to free up memory, say — until you reassign the variable, or the variable goes out of scope. If you reassign a variable containing a strong reference, the referenced object will be removed if no other variables reference it. This approach works well for simple situations, but can cause problems when references become cyclic: for example, when instance A holds a reference to instance B, and vice versa. Because these connections are usually strong, neither A nor B can ever be purged from memory, even if your program no longer requires them. The memory they occupy can never be freed for use by other tasks. For a couple of objects, that may not be a problem; agents have plenty (though not an infinite amount) of memory, and devices’ RAM is flushed whenever the unit deep-sleeps or is power-cycled. However, this might prove troublesome for applications that need to spawn a lot of cross-referencing instances. The more that are generated, the less memory can be recovered and you could well run out of RAM. This is called a memory leak.

For example, in the following code, each instance holds a strong reference (the value held in each instanceX variable) to each other. Even when one of the variables is reassigned, the referenced object still exists, as shown in the log readout:

class Item {
    myName = null;
    objectRef = null;

    constructor(name = "default") {
        myName = name;
    }

    function methodOne(anObject) {
        objectRef = anObject;
    }
}

local instanceOne = Test("I1");
local instanceTwo = Test("I2");

instanceOne.methodOne(instanceTwo);
instanceTwo.methodOne(instanceOne);

instanceTwo = null;
server.log(instanceOne.objectRef);
// Displays something like '(instance : 0x7ffab8f73700)'

Fortunately, Squirrel provides a way of managing this kind of situation: it allows you to create weak references to objects — and, indeed, functions, tables and arrays — which are references that do not claim ownership over those objects. If you assign a weak reference to a variable, the referenced object can be removed if necessary.

To generate a weak reference to an object, call its weakref() method. You can test whether a variable contains a weak reference by using the typeof command:

local instanceOne = Test("I1");

// Get a weak reference to 'instanceOne'
local weakRef = instanceOne.weakref();

// Confirm the reference type
server.log(typeof weakRef);
// Displays 'weakref' -- 'weakRef' is indeed a weak reference

Note It is also possible to call weakref() on an Integer, Float or Boolean variable, but in this case the call returns a copy of the variable (because those are scalar types that pass by value not by reference).

Let’s change the code shown a little further up to make use of weak references:

class Item {
    myName = null;
    weakObjectRef = null;

    constructor(name = "default") {
        myName = name;
    }

    function methodOne(anObject) {
        weakObjectRef = anObject.weakref();
    }
}

local instanceOne = Test("I1");
local instanceTwo = Test("I2");

instanceOne.methodOne(instanceTwo);
instanceTwo.methodOne(instanceOne);

instanceTwo = null;
server.log(instanceOne.weakObjectRef);
// Displays '(null : (nil))'

This time methodOne() causes as weak reference to be stored, and when we allow instanceTwo to be cleared (by reassigning the variable), Squirrel does so and the reference is displayed accordingly. When the object pointed by weak reference is destroyed, the weak reference is automatically set to null. Your code can check this to prevent it from attempting to access the purged object.

Note In standard Squirrel, the bindenv() function keeps a weak reference to the object, but in Electric Imp Squirrel, it is a strong reference.


Back to the top