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
// 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.
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.
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 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
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. The class and the 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."
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
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 (
front of the parameter name, it is copied directly to the field of the same
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
.field parameter has the same type as the field it assigns to.
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
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
In a class with inheritance you may want to invoke a constructor
for the class you are extending. This is done with the
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`.
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 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
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
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
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