CHAPTER 6: SQUIRRELS WITH CLASS



So far, you've learned how to use variables, functions, tables, and different data types for practical purposes. Now it's time to mix those all together into what is known as a class.


LESSON 6.1: HELLO CLASS

So what are classes, anyway? You remember your old friend, the table, right? Classes are like tables, except you can make multiple instances of them, and they can come with what's called a constructor, which is a function that tells a class instance what to do when it's made. Let's take a look at one.

//Class definition
class Foo{
	myNumber = 0;
	myName = "";

	constructor(name, number){
		myName = name;
		myNumber = number;
		
		print("Beware, I live! I am " + myName + "!");
	};
	
	function sayNumber(){
		print(myNumber);
	};
};

//Instance creation
local villain = Foo("Sinistar", 24);
villain.sayNumber();

The output for this should be Beware, I live! I am Sinistar!24.

You might have noticed a few things about this from looking at it. First, classes are defined by using the class keyword instead of local. Then, there's variables, known as member variables, which also don't use local. The constructor function, unlike other functions, does not use the function keyword either, but other functions do use it.

When creating an instance of a class, you assign a variable to the class, calling it like you would a function, with the arguments being those that you wrote into the constructor. Don't worry that there's no return statement in the constructor; it will always return the instance itself to be stored in the variable. Another thing to note about the constructor is that you must make different versions of its varaibles for use as arguments. Using the same names will just make new varaibles in the constructor's scope, meaning they'll still be their default values even after the constructor is done.

Alternatively, classes can also be defined using the insert operator, same with variables.

//Alternate class definition
Foo <- class{
	constructor(){
		print("I made a funny!");
	};
};

local dog = Foo();

It is conventional to have class names begin with capital letters.

EXERCISE 6.1

Create a class that takes a name and a relative. Have it print out "Hello, my name is [name]. You killed my [relative]. Prepare to die." Then give it a function called fight() that prints "En garde!" Create several instances, and give a different name and relative to each of them, then have the final one call fight().


LESSON 6.2: INHERITANCE

This has nothing to do with dragons. Actually, Squirrel classes are able to have children! Yay! Baby squirrels!... ahem.

Classes are able to pass down their traits to their 'children' by way of inheritance. This is done by defining a class with the extends keyword.

class Foo{ //This is the superclass
	myName = "";
	
	constructor(name){
		myName = name;
	};
};

class Pity extends Foo{ //This is the subclass
	myNumber = 0;
	
	function say(){
		print(myName + " " + myNumber);
	};
};

local mrT = Pity("Mr. T");
mrT.say();

You'll see that when the subclass is called, the constructor for the superclass is called for it. It also has its own myName variable, even though one was never defined for it. However, if you define a new constructor in the subclass, it will overwrite the constructor for the superclass.

class Foo{
	myName = "";
	
	constructor(name){
		myName = name;
		
		print("I, " + myName + ", honor my anscestors!");
	};
};

class Pity extends Foo{
	myNumber = 0;
	
	constructor(name){
		myName = name;
		
		print("I, " + myName + ", pity the dead foo!");
	};
};

class Honor extends Foo{};

local zuko = Honor("Zuko");
local mrT = Pity("Mr. T");

You'll notice that Honor does not even have a name variable of its own. It inherits everything from its parent class.

EXERCISE 6.2

Create a class called Lifeform that has variables for name and age. Make a constructor for it that prints "Hello, my name is [name], and I am [age] years old!" Make a child class for Lifeform called Human with a variable for job, and another child class for Lifeform called Dog. Give Dog a constructor that prints "Woof! (I can't talk, silly; I'm a dog.)" Create an instance of Dog with any name you like and as many Human, since Human is able to say its name.

LESSON 6.3: CLONING INSTANCES

When you pass a variable to a function, you pass it by value. Passing a class, however, passes its instance ID, not creating a duplicate of it. Take this for instance:

class Foo{
	myName = "";
	
	constructor(name){
		myName = name;
	};
};

local thing = Foo("Dave");

local stuff = thing;

stuff.myName = "Bob";

print(stuff.myName + " and " + thing.myName);

The output for this code would be Bob and Bob because you've assigned stuff to equal the instance ID of thing. This may not always be what you want. Sometimes, you want to create a class instance that's a duplicate, but not the same. For this, we use the clone keyword. By replacing the definition of stuff with local stuff = clone thing; we create a whole new instance using thing's data. Now, you can modify stuff without changing a thing. The new output will read Bob and Dave.

The same rule applies when passing an instance to a function. Let's look at a modified version of the code above.

class Foo{
	myName = "";
	
	constructor(name){
		myName = name;
	};
};

local thing = Foo("Dave");

local stuff = thing;

function changeName(instance, name){
	instance.myName = name;
};

changeName(stuff, "Bob");

print(stuff.myName + " and " + thing.myName);

The output for this script will still be Bob and Bob because the value of stuff still points to the address of the original instance, so when you change something wiht the argument, you change the original as well. In order to prevent this, you would pass a clone of the instance, however, as this creates a new instance on the heap, you will need to remember to delete the cloned instance using the delete keyword at the end of the function.

EXERCISE 6.3

Print an instance directly and see what the value looks like when printed out.


LESSON 6.4: METAMETHODS

You may have realized that if you were to pass an argument to a function of the wrong type, you could try to compensate for this by using functions like tointeger() to convert them, but what about class instances? There's a virtually unlimited number of different classes you could come up with, so if you had a function expecting, say, a player class, how does one check they're not getting an item or a block? Take this code for example:

class Foo{};

local stuff = Foo();

print(typeof stuff);

If you were to print this out, the output would say instance, even though the class name is Foo. This is the same case for every class instance you make... by default, that is. Thanks to metamethods, functions that objects have build into them by default, which can be redefined like many things in Squirrel, you can control how your class instances react to different operators, including the typeof operator. Let's modify Foo so that it prints out something more useful.

class Foo{
	function _typeof(){
		return "Foo";
	};
};

That's literally all it takes. Now, when you print the type, it will display Foo instead of instance. Using this, you can check an instance's type, and it doesn't even have to be different for each class!

These are the metamethods supported by Squirrel classes:
_typeof(): returns the instance type as a string.
_add(other): acts as + operator.
_sub(other): acts as - operator.
_mul(other): acts as * operator.
_div(other): acts as / operator.
_modulo(other): acts as % operator.
_add(other): acts as unary minus operator.
_tostring(): returns a string value of the instance ID in the form (instance : 0x00000000).
_set(index, value): sets the given index to the given value.
_get(index): returns the value of the given index.
_cmp(other): comparison operator, returns 1 if greater, -1 if less, and 0 if equal.

If you're confused about how _cmp(other) works, here's an example:

class Foo{
	number = 0;
	
	constructor(newNumber){
		number = newNumber;
	};
	
	function _cmp(other){
		if(number > other.number) return 1;
		if(number < other.number) return -1;
		return 0;
	};
};

Other metamethods exist, but will require a more in-depth explanation.

EXERCISE 6.4

Create a two classes, and give each one a metamethod for _add. In one class, make it return the sum of a number variable inside them, and for the other, have it join a string variable together. Print the results of both. Now, add a check to the number class's _add function so that it prints a warning when adding the wrong class instead of making an error.

Are you enjoying this tutorial? If so, why not send a donation? You can do so by clicking the donate link at the top of the screen, and if you do, please attach a note to your payment letting me know it's for the tutorial!

<< PREV | NEXT >>