Classes
In Toit, everything is an object, including things that may be non-object
"primitive types" in other languages. For example, an integer
is an object, and has methods like abs
:
// Method that takes an `int` and returns an `int`. foo x/int -> int: return x.abs // The absolute value of x.
To create custom objects, we must first define a class:
Here we have given our new class two fields, with integer
types, and they are
by default initialized to 42 and 103. We create new instance objects of the
class by naming the class, no new
keyword is needed:
There are various ways to declare fields in objects:
class Foo: // u is untyped and initialized to zero. u := 0 // v is an integer. v /int := 0 // w is a nullable integer (either null or an integer). w /int? := null // x is an integer, and it must be set by the constructor. x /int := ? // y is an integer, the field is immutable, the // constructor must set it. y /int // z has the same properties as y, but the short form is // better style. z /int ::= ? // LTUAE is a constant. When possible, prefer static // constants. LTUAE /int ::= 42 // PASSWORD is a static constant. static PASSWORD /string ::= "hunter2" constructor: x = y = z = 0 // Set the fields that must be set.
Getters and Setters
In Toit the external syntax for getters and setters is the same as for public fields:
class Bar: x /int := ? constructor .x: my-function bar/Bar -> none: // Accesses public field x, or uses the x getter. print bar.x // Sets the public field x, or uses the x setter. bar.x = 42
If we later decide to make the x field private we rename it to have a final underscore. At the same time, we can introduce getters and setters that keep our class compatible with the old version:
class Bar: x_ /int := ? // Private field. constructor .x_: // Getter, returns an int. x -> int: return x_ // Setter, takes an int, returns nothing. x= value/int -> none: x_ = value print "x_ was set to $x_"
Inheritance
Toit is an object-oriented language. The inheritance and typing mechanism for classes is similar to the one of Java: Classes can extend each other, with subclasses being subtypes in the typing system.
class Point: x /int := 0 y /int := 0 class Point3D extends Point: z /int := 0 main: p /Point? := null // Local variable p has type Point (nullable). p = Point // Create point. // Since a Point3D is a type of point we can // assign p to refer to a Point3D. p = Point3D // Create 3d point. p.x = 42 // OK because Point has a field called x. p.z = 103 // Compile-time error: Point has no z field! p3 := p as Point3D // Cast p to a Point3D (with run-time check). p3.z = 103 // OK because p3 has the right static type.
Interfaces
A class can only extend one class, but can implement several interfaces. Like classes, interfaces act as types in Toit's type system, but unlike classes, they do not include implementations for the non-static methods they declare. Since a method signature declared in an interface has no implementation, the colon at the end of the first line is omitted:
// To implement the Turtle interface, a class must have // these two methods. interface Turtle: forwards mm/int -> none turn degrees/int -> none // To be used as a Drawable, a class must have a method // called draw, which takes a Turtle and returns no value. interface Drawable: // No colon on the method declaration draw turtle/Turtle -> none // To have the type `Drawable` the Square class must both // declare that it `implements Drawable`, and contain the // `draw` method. class Square implements Drawable: draw turtle/Turtle -> none: 4.repeat: turtle.forwards 10 turtle.turn 90 main: // Variables can have interface types. Here `d` has the // type `Drawable`. d /Drawable := Square
In Toit an interface can have static methods and constructors, but the constructors must be factory constructors.
An abstract class has some similarities to a class and to an interface. Like a class, it can have fields and methods that its extending classes will inherit. Like an interface, it can have methods without implementations and constructors. The class and the non-static methods must be marked explicitly as abstract. A class cannot be instantiated (objects of that class cannot be created) unless it implements all the required methods from the abstract classes it extends:
// Classes that extend `TurtleBase` must implement two // methods. abstract class TurtleBase: mileage := 0 // Classes that extend `TurtleBase` should call this after // moving a distance to register the distance the Turtle // has moved. register-mileage mm/int -> none: mileage += mm // Concrete (non-abstract) classes that extend TurtleBase // must implement these two methods. abstract forwards mm/int -> none abstract turn degrees/int -> none // The LogTurtle doesn't actually perform the drawing moves, // it just logs the moves a real turtle would make. class LogTurtle extends TurtleBase: forwards mm/int -> none: print "I moved forwards $(mm)mm." register-mileage mm turn degrees/int -> none: print "I turned $degrees degrees" main: turtle := LogTurtle 4.repeat: turtle.forwards 10 // >> I moved forwards 10mm. turtle.turn 90 // >> I turned 90 degrees. print "The turtle has travelled $(turtle.mileage)mm."
Constructors
Sometimes it is not enough to specify the initial values for fields. When there
are more complex ways to initialize an object we make use of special functions
called constructors. In Toit the constructor has the name constructor
rather
than having the same name as the class:
import math class Vector: x /float y /float length /float constructor x-arg/float y-arg/float: x = x-arg y = y-arg length = math.sqrt x * x + y * y
It is rather common that parameters of constructors are copied directly into
fields of the new object, so there is a shorthand. By putting a dot (.
) in
front of the parameter name, it is copied directly to the field of the same
name:
import math class Vector: x /float y /float length /float // The dots initialize fields x and y. No need to restate // the types of x and y here. constructor .x .y: length = math.sqrt x * x + y * y
The shorthand .field
parameter has the same type as the field it assigns to.
Named Constructors
By default, constructors have no name and are invoked with
the name of the class. For example, the above unnamed constructor for the
Vector
class is invoked using the name Vector
:
main: // Create a new Vector using the unnamed constructor. // Note that there is no `new` keyword in Toit. v := Vector 10.0 7.5
We can have several unnamed constructors with different numbers of arguments. This is a natural result of the fact that Toit has argument-count overloading and argument-name overloading.
However, sometimes it is more helpful to have constructors with different
names. For example, the Vector
class might have a polar constructor:
import math class Vector: x /float y /float length /float constructor .x .y: length = math.sqrt x * x + y * y constructor.polar angle/float .length: // The dot above initializes the length field. x = length * (math.cos angle) y = length * (math.sin angle) main: // Use the unnamed Cartesian constructor. v := Vector 10.0 7.5 // Use the polar constructor. v2 := Vector.polar math.PI/6 8.0
Super-constructors
In a class with inheritance you may want to invoke a constructor
for the class you are extending. This is done with the super
keyword:
class Color: r /int g /int b /int constructor .r .g .b: class ColoredVector extends Vector: color /Color constructor x/float y/float .color: super x y // Calls the constructor of `Vector`.
Factory constructors
A factory constructor is rather like a static method that returns an instance of the class. Simply placing a return statement in a constructor makes it into a factory constructor.
For example, it may be very common to create a special
instance of an immutable class. Since it is immutable,
the constructor can save memory by returning the same
object every time. Here we have a factory constructor
named origin
.
class Pair: x /int y /int // Regular constructor. constructor .x .y: // Private singleton object. static SINGLETON-ORIGIN_ ::= Pair 0 0 // Named factory constructor. constructor.origin: return SINGLETON-ORIGIN_
Factory constructors are the only kinds of constructors that interfaces can have. You cannot instantiate an interface, so regular constructors are not possible. An example of this is ByteArray, which is actually an interface because there are several different implementations. The constructors return a "regular" ByteArray:
ba1 := ByteArray 10 // Constructs a ByteArray with 10 zeros. ba2 := ByteArray 5: it * 2 // Constructs #[0, 2, 4, 6, 8]
Advanced constructor topics
Some languages have a different syntax for initializing the field variables. For example, C++ has the initializer list which can precede the constructor body. In Dart a similar feature is also called initializer lists. Toit uses the same syntax for member initialization as for the rest of the constructor, so you don't have to learn two different syntaxes.
Usually you don't have to worry about where the boundary is between
field initialization and the rest of the constructor, but there can
be situations where it matters. Implicitly, a constructor is split into two
parts: the initialization, and the instance part. They are separated by the
super
call.
If no super
call is present, then Toit implicitly adds
one as late as possible. This is either at the end of the constructor body, or
as soon the code uses this
, a reference to the newly constructed object.
Implicit uses of this
also count, for example by invoking a method, or by
creating a lambda that captures this
.
class Point: x/float // This must be set during field initialization... y/float // ...of the constructor. // A method on the Point class stringify -> string: return "$x, $y" constructor x-arg/float y-arg/float: // Calling the stringify method uses "this". print "Created " + stringify x = x-arg // Error - this happens in the instance... y = y-arg // ...part of the constructor.
Although the compiler will infer the boundary between field
initialization and object creation you can always explicitly specify
it by calling the super
constructor. Even classes that don't
explicitly extend another class extend the Object
class so
you can call the super
constructor with no arguments:
class Foo: x /int constructor: x = 12 // Explicit 'super' calls are possible but rare. super // Explicit `this` is also not normally needed. print this.x
Before the object is created you can already refer to fields that
have been assigned. These field accesses do not count as instance accesses.
Immutable fields do not become immutable before the call to super
in the constructor..
This still means the fields are immutable all other places than in the
constructor, since no other parts of the program can obtain a reference
to the newly constructed object before the super
call.
import math class Vector: x/float // These must be initialized in the initializer... y/float // ...part of the constructor, not later. length /float // A method on the Vector class stringify -> string: return "$x, $y" constructor.unit: x = 1.0 y = 1.0 // It is OK to access fields in the initializer part of // the constructor. length = math.sqrt x * x + y * y super // Implicit use of this when calling stringify. print stringify