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.