slate

Objects

An object is a collection of fields.

print({ name: "ada", born: 1815 })
print({ "two words": 1 })
print({})
{name: "ada", born: 1815}
{"two words": 1}
{}

A field is written key: value, and { name } is shorthand for { name: name }. It is the same spelling an object pattern has, read the other way round: on the left of an = it takes a field apart, and on the right it puts one together.

val name = "ada"
val born = 1815

print({ name, born })
print({ name, born, alive: false })
{name: "ada", born: 1815}
{name: "ada", born: 1815, alive: false}

A shorthand needs a name a program could bind, so a keyword and a quoted key each have to say what their value is; and { name? }, which marks a field a pattern need not find, is a pattern’s spelling and means nothing in a literal.

print({ if })
is a word the grammar has taken

A key in a literal is a name or a string, and nothing else — a number there is a parse error. A key written through an index may be any value, and stays that value:

var q = {}

q[42] = "answer"

print(q, q[42])
{42: "answer"} answer

That is why toJSON refuses a non-string key rather than rendering it: { 1: "a" } and { "1": "a" } are two objects and would be one document.

Objects are reference types and compare by their contents.

keys(o), values(o), entries(o) and has(o, k) are how a program walks one; entries is what makes a destructuring for head worth having:

val o = { a: 1, b: 2 }

print(keys(o), values(o), has(o, "a"))

for [k, v] in entries(o)
    print(k, v)
["a", "b"] [1, 2] true
a 1
b 2

has is the presence test, and it is the only one. A field holding null and a field that is not there both answer true to == null — the one special case in == — so where the difference matters, has is what draws it. It asks about the object’s own fields; one reached through a proto is not counted.

o with { f: v } answers a copy with f changed. There is no spread in a literal — with is it.

The braces after with are read exactly as a literal’s are, shorthand and all, so o with { x } is o with { x: x } and a keyword, a quoted key and a name? each say what to write instead there too:

val x = 1
val y = 2
val base = { a: 9, x: 0 }

print(base with { x })
print(base with { x, y })
print(base with { x, y: 7 })
print(base)
{a: 9, x: 1}
{a: 9, x: 1, y: 2}
{a: 9, x: 1, y: 7}
{a: 9, x: 0}
val o = { a: 1 }

print(o with { if })
is a word the grammar has taken

freeze and isFrozen

freeze(v) closes a value to writing and answers the very value it was given. Reads are untouched; every write is refused with a sentence naming what was refused and where the copy is:

val settings = freeze({ port: 8080, host: "localhost" })

print(settings.port, isFrozen(settings))

settings.port = 9090
`port` belongs to a frozen value

It closes an object, an array, a set, a map, a buffer and a weak map. A value with nothing to write through answers itself — a number, a string, a function — and isFrozen says true of one, so isFrozen(freeze(x)) holds for every x. Freezing twice is a no-op, which is what lets a constructor freeze what it was handed without asking whether somebody else did.

It is shallow, which is JavaScript’s rule, and the rule worth stating: freezing an object says nothing about the objects its fields hold. A program that wants the whole graph walks it itself.

A copy is free to change, which is what every refusal points at — with and a spread for an object, [...xs] for an array, Set(s) and Map(m) for the two collections:

val base = freeze({ port: 8080 })
val mine = base with { port: 9090 }

mine.port = 3000

val xs = freeze([1, 2])
val ys = [...xs]

push(ys, 3)

print(mine.port, isFrozen(mine), ys, isFrozen(xs))
3000 false [1, 2, 3] true

A data variant is frozen without anybody having called freeze, and says so differently — its refusal names with as the line to write instead, where a frozen value’s names it as the way back to something writable.

proto

proto is an ordinary field, and a lookup that misses carries on into it:

val Shape = {
    describe: self -> s"${self.kind} with area ${self.area()}",
    kind: "shape"
}

val Square = { proto: Shape, kind: "square", area: self -> self.side * self.side }

square(side) = { side: side, proto: Square }

print(square(4).describe())
square with area 16

No syntax and no new kind of value. A proto may have a proto, so chains and overriding come free; describe lives on Shape and calls area, which only the concrete shapes have, so the call goes back down to whichever object it started from. That is dispatch, and it needed no keyword.

It is also what makes objects affordable. Three methods written as captured closures cost three closures per instance; on a proto they cost three once.

An object pattern counts a field a proto supplies, a pattern asking whether the value has the field — which is the question . answers. k in o asks it too, as in JavaScript: whether o[k] would find a member there, own or inherited, without reading it. has is the other side of the seam and asks about the object’s own fields only.

o[k] reads what o.k reads, so a name worked out at run time finds an inherited member exactly as a written one does — JavaScript’s rule. Everything that reports what an object HOLDS stays its own: has is JavaScript’s Object.hasOwn, keys and entries list the object’s own fields, and o[k] = v writes to the object itself, even where the value it replaces was inherited:

val Shape = { kind: "shape", sides: 0 }
var tri = { proto: Shape, sides: 3 }
val name = "kind"

print(tri[name], tri["sides"], has(tri, name), keys(tri))

tri[name] = "triangle"

print(tri.kind, Shape.kind, has(tri, name))
shape 3 false ["proto", "sides"]
triangle shape true

A string key on a builtin kind reads that kind’s property or method the same way, so xs["length"] is the length; a name the kind does not have is still the fault an index into it gives.

in and has split along the same line — in follows the chain and has does not:

val Shape = { kind: "shape" }
val tri = { proto: Shape, sides: 3 }

print("kind" in tri, has(tri, "kind"), "sides" in tri, "toString" in tri, "colour" in tri)
true false true true false

The receiver rule

A method reached through a proto is handed the object it was found on. One describe serves every shape, so it cannot have captured a particular one — it has to be told, and self is an ordinary first parameter.

A method stored on the object itself has already captured what it needs and is given nothing extra:

counter()
    var n = 0
    var c = {}

    c.bump = () ->
        n += 1
        n

    c

val c = counter()

print(c.bump(), c.bump())
1 2

That is not two rules but one: captured methods take no receiver, shared ones must.

Only a call passes a receiver: o.m(...), and o[k](...) exactly where o.k(...) would, which is JavaScript’s rule — guarded or not, so o.m?.(...) and o[k]?.(...) pass it too (expressions). o.m on its own hands back the bare function, so taking a method off an object and calling it later is allowed and gives you what you took — and o["m"] is the same bare function:

val Shape = { describe: (self) -> "a shape with " + string(self.sides) + " sides" }
val tri = { proto: Shape, sides: 3 }
val word = "describe"

val bare = tri[word]

print(tri[word]())
print(bare(tri))            // read out and called later, it is handed what it is given
a shape with 3 sides
a shape with 3 sides

A function the object holds itself takes no receiver whichever way it is reached, and an array’s element is called with exactly what was written.

Operator methods

An object may answer for an operator, and the operator is the method’s name. A class body writes one with the definition syntax it already has, so the definition and every call that reaches it read alike:

class Money
    var cents

    +(self, o)   = Money(self.cents + o.cents)
    *(self, n)   = Money(self.cents * n)
    unary_-(self) = Money(-self.cents)
    <=>(self, o) = self.cents - o.cents      // `<`, `<=`, `>` and `>=` read its sign

    toString(self) = "$" + string(self.cents \ 100)

val a = Money(150)
val b = Money(50)

print(a + b, a * 2, -a)
print(a < b, a > b)
$2 $3 $-1
false true
methodoperator
++
--
**
//
\\
%%
unary_-prefix -
<, <=, >, >=those four, each on its own
<=>all four of them, from one method
====, and != as its opposite
hasha table key

unary_- is spelled apart from - because it takes no other side: a class whose - has two operands has said nothing about what -v should be.

Nothing else may be named. The bitwise operators, && and ! are all questions about bits or about truth, which is not a thing a value object has an opinion on, and a class body naming one is refused where it is written.

A class orders itself in one of two ways and never both. Either it writes the four comparisons, each saying exactly what that operator means for it, or it writes <=> and lets all four read the sign of one number:

class Version
    var n

    <(self, o)  = self.n < o.n
    <=(self, o) = self.n <= o.n
    >(self, o)  = self.n > o.n
    >=(self, o) = self.n >= o.n

print(Version(1) < Version(2), Version(2) <= Version(2))
true true

Writing both is refused, the two being two chances for a type to order inconsistently with itself:

class Version
    var n

    <(self, o)   = self.n < o.n
    <=>(self, o) = self.n - o.n
writes one or the other

== is a method of its own and is not routed through <=>, because a type whose ordering is coarser than its equality — a case-insensitive name, a version with build metadata — is an ordinary thing to want. != is always the opposite of == and a class may not write one:

class Tag
    var t

    !=(self, o) = true
is always the opposite of

The left operand decides and the right is never asked — a.equals(b)‘s rule already — so there is no reflected form and 2 * money is a fault.

A method is the last thing tried, so none can shadow what an operator already means.

An operator method is an ordinary member, so a.+(b) calls what a + b calls and a word-named method has no operator meaning at all: a class writing plus has written a method called plus.

A type that writes == should write hash beside it, or two equal values will not find each other in a table.

toString and toJSON

A class may say how it prints and how it encodes, which is what a value object needs to stop leaking the fields it is made of:

class Money
    var cents

    toString(self) = "$" + string(self.cents \ 100)
    toJSON(self)   = string(self.cents \ 100)

print(Money(150))
print(toJSON({ paid: Money(150) }))
$1
{"paid":"1"}

Both replace everything below them at every depth, so a value inside an array or a response body renders the way its class says rather than only when printed on its own.

Without toJSON, a class instance and a data variant encode as their own fields — Circle(3) is {"r":3} — and never as the chain they hang from.

Identity

The proto is the identity, and class is what lets is ask about it. Written by hand, p.proto == Point is the closest an object literal gets — and it is not quite instanceof: == on objects is deep, so it answers true for anything holding the same fields, and it looks exactly one link up the chain.

Search

Esc
↑↓ to navigate ↵ to open Esc to close