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.

A val does not change, so assigning to one is refused — through an object of the class and through the class itself alike:

class Coin
    val kind = "coin"
    var side = 1

val c = Coin()

c.kind = "token"
is a `val` of `Coin`

Without the refusal the write would be taken and would put a field on c that hides the class’s value: c.kind would answer "token" while Coin.kind still answered "coin", and nothing would have said so. A var is the field each object gets and is written freely:

class Coin
    val kind = "coin"
    var side = 1

val c = Coin(2)

c.side = 5

print(c.kind, c.side)
coin 5

Annotating a field

A field is a binding like any var, so it takes the same annotation: every type a binding may say — a word, a union, a function type, array of T, Map[K, V], a tuple, a shape, or the class’s own name. A val on the class takes one too:

class Counter
    var count: integer = 0
    var step: integer = 1
    var label: string | null = null
    var onChange: (integer) -> string = n -> s"now ${n}"
    val most: integer = 10

    bump(self)
        self.count += self.step
        self.onChange(self.count)

val c = Counter()

print(c.bump(), Counter.most)
now 1 10

It is checked where the object is made, because that is where the value arrives: the generated new checks each annotated field exactly as new(count: integer) would check its parameter, and an initialiser is checked as the default it is.

class Counter
    var count: integer = 0

readCount(text) = Counter(text).count

print(readCount("seven"))
`count` was declared integer, and was given "seven"

An assignment is held to it as well — the annotated var rule. Where the object’s class is beyond doubt, a val bound to Counter(), a write of another kind is refused before the program runs:

class Counter
    var count: integer = 0

val c = Counter()

c.count = "lots"
`count` on `Counter` was declared integer, and this is string

A written new checks what its own parameters declare, and a field it fills from an initialiser is checked there.

No member may be called proto

proto is the link itself, not a name a class can spend. A class is an object with a proto, and every object it makes is given one pointing back at the class — that is the whole of how a method is found. A member declared under that name would be sitting behind the link: t.proto walks to the link first and answers the class, and the declared value only appears once something has assigned to it. So it is refused where it is written, in every form that declares one — var, val, a new parameter, a method, a getter, a setter, and a data variant’s field:

class Thing
    var proto = 2
`proto` is how an object reaches what it was made from, so class `Thing` cannot call a member that

This is a rule about class bodies and changes nothing about proto on an ordinary object. A literal is where a program writes the link by hand, which is how protos worked before class existed, so { proto: 2 } is an ordinary field holding an ordinary integer and { proto: Thing } is still the link a lookup walks:

class Greeter
    hi(self) = "hi"

print({ proto: 2 }.proto)
print({ proto: Greeter }.hi())
2
hi

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.

Properties

get in front of a definition makes a field that is worked out, and set makes one that is written through. Both are the definition syntax the rest of the body uses, and self is the object exactly as it is for a method — what changes is how they are reached: c.area, with no brackets:

class Rect
    var w
    var h

    get area(self) = self.w * self.h

    get width(self) = self.w

    set width(self, v)
        self.w = v

val r = Rect(3, 4)

print(r.area, r.width)

r.width = 10

print(r.area, r.width)
12 3
40 10

get and set are soft words, so a class that already has a method called get keeps it — what tells them apart is the name after the word.

A property lives on the class, as a method does, so it costs an object nothing, a subclass overrides one by writing another, and a getter may read a getter:

class Circle
    var r

    get diameter(self) = self.r * 2

    get area(self) = self.diameter * self.diameter

print(Circle(2).diameter, Circle(2).area)
4 16

A getter is not a method, and calling one is a fault rather than a call of whatever it answered:

class Rect
    var w
    var h

    get area(self) = self.w * self.h

print(Rect(3, 4).area())
is a property, not a method

What a property is not

A property is computed and not stored, so nothing that reports what an object HOLDS mentions one. That is one rule and it settles four questions at once — keys, printing, toJSON and a class pattern all answer about fields, and with copies fields:

class Rect
    var w
    var h

    get area(self) = self.w * self.h

tell(v) = v match
    Rect(w, h) -> s"${w} by ${h}"
    _ -> "something else"

val r = Rect(3, 4)

print(keys(r), r, toJSON(r))
print(tell(r), (r with { w: 10 }).area)
["w", "h"] Rect(w = 3, h = 4) {"w":3,"h":4}
3 by 4 40

A class that wants area in its JSON writes a toJSON saying so, which is what that hook is for.

r["area"] reads the property too, a computed name reading exactly what . reads — JavaScript’s rule, and the one objects gives for every inherited member. has(r, "area") is still false, has asking only about what the object holds.

A property and a field of one name are refused, being one name to a reader and to the . that reaches both:

class Rect
    var area

    get area(self) = 1
given twice

get area beside set area is the pair the two words exist for and is not a clash.

A property with one half

A get with no set makes the property read-only, and the fault names it:

class Rect
    var w
    var h

    get area(self) = self.w * self.h

val r = Rect(3, 4)

r.area = 12
`area` is read-only on Rect

A set with no get is the same thing facing the other way — a write-only property, where the read is the mistake. Without that, storing through such a property and reading it back would answer null with nothing anywhere to say why:

class Box
    var n

    set doubled(self, v)
        self.n = v * 2

val b = Box(1)

print(b.doubled)
`doubled` is write-only on Box

A set may say what it takes, and then it is checked where the assignment is written, which is what an annotated var already buys:

class Temperature
    var celsius = 0

    get f(self) = self.celsius * 9 \ 5 + 32

    set f(self, v: integer)
        self.celsius = (v - 32) * 5 \ 9

val t = Temperature()

t.f = "hot"
was declared integer

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.

Inheritance and construction

from takes an expression — a class in the same file, an imported one, a module’s member, or one a function chose while the program ran — so a subclass is built through its base at run time, which is JavaScript’s rule.

A subclass that writes no new of its own takes its base’s. It is called with the same arguments, the base’s defaults and hand-written body run, and self in that body is already the subclass’s instance. Then the subclass’s own initialised fields are set, and the object’s proto is the subclass:

class Notes
    var served = 0
    var log = []

    new(var owner)
        self.log.push(s"opened by ${owner}, archive: ${self is Archive}")

    show(self) = 2

    serve(self)
        self.served += 1

class Archive from Notes
    var shelf = "A"

    show(self) = 4

class Vault from Archive
    sealed(self) = true

val v = Vault("ed")

v.serve()
v.serve()
print(v.show(), v.served, v.shelf, v.log)
print(v is Vault, v is Archive, v is Notes, Notes("jo") is Archive)
print("sealed" in v, "sealed" in Archive("jo"))
4 2 A ["opened by ed, archive: true"]
true true true false
true false

The inherited serve changes served on the Vault, and each instance has its own served and its own log, exactly as if the class had declared them itself.

A field the subclass gives an initialiser is not a parameter — TypeScript’s reading — so Archive("ed", "B") still gives shelf its "A". Set it in the body of a new of your own, or after the object is made.

A base whose new is the plain form works the same way, and what its body answers becomes an instance of the subclass:

class Plain
    new(v) = { v: v }

class Doubled from Plain
    twice(self) = self.v * 2

print(Doubled(5).twice(), Doubled(5) is Doubled, Doubled(5) is Plain)
10 true true

A subclass that writes its own new does not call the base’s. It starts from what every base’s initialisers make — each base var with an initialiser has its value, one with none is absent — and then its own new runs:

class Holder
    var count = 5
    var name

class Own from Holder
    new(var tag)
        self.count += 1

val o = Own("x")

print(o.count, o.tag, has(o, "name"), o is Own, o is Holder)
6 x false true true

A positional class pattern sees the subclass’s own fields and none of its bases’. The base’s fields are the base’s to destructure; name them with the object form:

class Point
    var x
    var y

class Labelled from Point
    var label = "p"

tell(v) = v match
    Labelled(l) -> s"labelled ${l}"
    _ -> "something else"

val q = Labelled(1, 2)

print(tell(q), q match
    Labelled { x, y } -> x + y
    _ -> 0)
labelled p 3

A subclass that declares no fields over a base that has no new has nothing to make, and says so when it is called:

class Shape
    describe(self) = "a shape"

class Hollow from Shape

print(Hollow())
class `Hollow` has nothing to make

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