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}
{}

Every field is written key: value. There is no { name } shorthand in a literal — that spelling belongs to an object pattern, where it means bind the field name to the name name, and a literal reading it the other way round would make one notation mean two things.

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

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

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. (JavaScript splits the same seam and puts in on this side of it.)

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 o.m(...) passes a receiver. 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.

Operator hooks

An object may answer for an operator. The word is the method’s name, and a class body writes one with the definition syntax it already has:

class Money
    var cents

    plus(self, o)    = Money(self.cents + o.cents)
    times(self, n)   = Money(self.cents * n)
    negated(self)    = Money(-self.cents)
    compare(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
hookoperator
plus+
minus-
times*
dividedBy/
remainder%
negatedprefix -
compare<, <=, >, >=
equals==, !=
hasha table key

Ordering is one hook and not four, so a type cannot order inconsistently with itself. == keeps equals, because a type whose ordering is coarser than its equality is an ordinary thing to want.

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

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

A type that writes equals 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