slate

Classes

A class is an object with a proto and a word in front of it. class binds a name to an object literal and from is that object’s proto, so what runs is what protos already did.

class Shape
    val sides = 0

    describe(self) = s"a ${self.name()} of area ${self.area()}"

class Square from Shape
    val sides = 4
    var side

    name(self) = "square"

    area(self) = self.side * self.side

print(Square.new(4).describe())
a square of area 16

A class belongs to the top level of a file, as a type does and for the same reason: its name is resolved while compiling. class is a soft word, so a program already using it as a variable name is untouched.

What the word buys

A method is written in the ordinary definition syntax. A field’s value is an expression, so a method in a literal is a lambda and its body is whatever fits after ->. A definition’s body may be an indented run of statements, take annotated or destructured parameters, be async, or yield. A class of twenty methods reads like twenty functions rather than like twenty fields.

is asks which class a value was made from, and walks the whole chain. This is the one thing class adds that a hand-written proto could not have:

class Shape
class Square from Shape
    var side

val sq = Square.new(1)

print(sq is Square, sq is Shape)
print({ side: 1, proto: Square } is Square)
print({ side: 1 } is Square)
print(Square is Square)
true true
true
false
false

A bare name in pattern position is a binding unless something has declared it, and a class declaration is what declares it — so the name works everywhere a type does: in is, in a match arm, and in a parameter’s annotation. A class crosses a file under export as both halves at once, the value and the type.

val and var

val is the class’s and var is each object’s, and that distinction is the one that bites. A val is one value however many objects there are; a var declares a field each object gets, and its initialiser runs once per object:

class Bag
    var items = []          // a NEW array for every bag
    val kind = "bag"        // one string, shared

val a = Bag()
val b = Bag()

push(a.items, 1)

print(a.items, b.items, a.kind)
[1] [] bag

A mutable literal under val is refused, and the message names var as the fix:

class Bag
    val items = []
var

TypeScript spells the per-instance one items = [], so a reader coming from TS writes val and gets one array every instance pushes into. A mutable literal under val is therefore refused, and the message names var as the fix. Only a literal is refused: an object bound outside the class and named here is sharing somebody asked for, and still compiles.

The generated new

A class that declares fields and writes no constructor is given one, taking all of them — the fields with no initialiser first, then the initialised ones, each optional with its initialiser as its default:

class Square
    var side
    var tags = []

print(Square.new(4))            // tags is a fresh []
print(Square.new(4, ["red"]))   // and tags said otherwise
Square(side = 4, tags = [])
Square(side = 4, tags = ["red"])

A default is worked out at the call, so leaving tags out still gives every square its own array. An initialiser may read a field bound before it — var side then var area = side * side.

An initialised field comes after an uninitialised one whatever order they were written in, since a default has to be trailing: var kind = "plain" above var side gives new(side, kind = "plain").

A class that declares nothing gets no new at all, rather than one answering an empty object; { side: 4, proto: Square } still works and is how such a class is made.

Writing your own new

Write one when it has something to do. var in its parameter list declares the field and assigns it — TypeScript’s parameter property — and self is the object being made. The body runs for its effect and the object is what comes back:

class Rect
    var area = 0

    new(var w, var h)
        if w < 0 || h < 0 then throw "a side cannot be negative"

        self.area = w * h

print(Rect(3, 4).area)
print(Rect(-1, 4).area catch e -> e.message)
12
a side cannot be negative

A class with no declared fields keeps the plain form, where the body’s value is the object: new(v) = { v: v }. It is given the class as its proto on the way out.

Class patterns

A class name written with fields after it tests and takes apart at once, which is what a Scala case class does — by position, or by name:

class Square
    var side

class Circle
    var radius

class Rect
    var w
    var h

tell(v) = v match
    Square(n) -> s"a square of side ${n}"
    Circle { radius: r } -> s"a circle of ${r}"
    Rect { w, h } -> s"${w} by ${h}"
    _ -> "something else"

print(tell(Square(3)), tell(Circle(7)), tell(Rect(2, 5)), tell(42))
a square of side 3 a circle of 7 2 by 5 something else

The positional order is the constructor’s, and that holds by construction rather than by convention — the field list a pattern is checked against is the parameter list of the new the class was given, so Square(n) binds what Square.new(4) sets and the two cannot drift apart.

A class that builds its own object in a hand-written new has no such list and is refused the positional form by name, with the named one offered instead.

Naming the class is what lets a misspelled field be caught: a bare { raduis: r } is a legal pattern that never matches, where Circle { raduis: r } is refused where it stands.

The class name is also a shape value, so Circle.test(v) asks at run time what v is Circle asks where it is written, and Circle.mismatch(v) and Circle.name() answer too — see Types. A static the class declares under one of those three names wins over it.

is in the header

is in a class header is TypeScript’s implements — a promise, checked where the class is written:

type Drawable = { draw: function }

class Pen is Drawable
    var ink

    draw(self) = "pen with " + self.ink

print(Pen("blue").draw())
pen with blue

Leave draw out and the fault names the class, rather than arriving wherever something first wanted to draw one:

type Drawable = { draw: function }

class Pen is Drawable
    var ink

    scribble(self) = "pen"
Drawable

It inherits nothing; from does that. What it buys is when you find out: leave draw out and the fault names the class rather than arriving wherever something first wanted to draw one. A class may promise several types (is Drawable, Named) and descend from one.

A promise is kept by a method a base supplies, because an object pattern counts a field a proto supplies.

There is no super

And none is needed. A base class is an ordinary value in scope, and a method stored on it directly is handed no receiver — so passing one is how you call it:

class Shape
    describe(self) = s"a shape of area ${self.area()}"

class Square from Shape
    var side

    area(self) = self.side * self.side

class Loud from Square
    new(side) = { side: side }

    describe(self) = upper(Shape.describe(self)) + "!"

print(Loud(2).describe())
A SHAPE OF AREA 4!

Shape.describe(self) names the class the call was written in, which is what a super call means. Counting links from whatever object turned up — self.proto.proto — is a different and wrong thing.

Calling a class

An object with a new is callable, so Square(4) is Square.new(4). That is what makes a data variant‘s constructor an ordinary function.

Search

Esc
to navigate to open Esc to close