slate

External

external localStorage

external names a value of the JavaScript host and binds it to a slate name. It is the door through which a browser API or an npm package reaches a program, and it is a language feature rather than a library one: what used to need a hand-written builtin in the back end — thirteen of them are slate:dom — is a package’s own line now.

It is the JavaScript host’s alone. Under the interpreter the declaration is accepted and every operation that would touch the host faults, naming the command, exactly as slate:dom does — the program is correct and the way it was run is the mistake.

The declaration

external localStorage
external ResizeObserver
external clipboard = "navigator.clipboard"

A bare name reads that name off the global object; a = names a dotted path instead and binds the last thing on it under the slate name on the left. Nothing else is a path: there are no arguments, no call, and no subscript in one — a path is how a global is reached, and everything after that is an ordinary operation on the value.

A declaration stands at the top level of a file and nowhere else, which is where import stands and for the same reason: it says what the file is built on. One inside a function is refused where it is written.

main()
    external localStorage

    print(1)

main()
top level

The path is read where the declaration stands, and a path that names nothing is a fault saying so and quoting the path. A global that does not exist is nearly always a program in the wrong host — a node build reaching for document, a page reaching for process — and the earliest place that can be said is the line that asked for it.

The name binds like a val. It is immutable, it is exported and imported like any other name, and it shadows and is shadowed by the ordinary rules. A declaration is not a type, an interface, or a promise about what the host value can do: it says only where it came from.

What an external is

An external is one value kind and it is opaque. It is not any, it is not an object, and it is not a bag slate can look inside. What can be done with one is a closed list, and everything on that list is an operation the host performs:

x.nameread a property
x.name = vwrite a property
x[k] and x[k] = vthe same two by computed key
x.name(a, b)call a method, with x as the receiver
x(a, b)call it, with the object its path named it from as the receiver
x.new(a, b)construct with it — JavaScript’s new
x is externalthe type test
x == yidentity

Everything else is refused, and the two halves of that are different in kind. Arithmetic, comparison, iteration and indexing-as-a-sequence have no meaning on one, and the checker refuses them before the program runs:

external localStorage

print(localStorage + 1)
does not apply to external

And a structural pattern simply does not match, because nothing a pattern does may fault. An external matches a bare name, _, and no other pattern:

external localStorage

print(localStorage match
    n @ number -> "a number"
    { length } -> "an object with a length"
    _ -> "only the name matched")
only the name matched

An external is a VALUE, so storing one is ordinary. It goes in an array, in a field, in a Map, through a function and back out — the restriction is on operations, never on travel. A handle that could not be kept would be no use to a program that has to hold a ResizeObserver for as long as the element it watches.

external localStorage

val kept = [localStorage]

print(kept.length, kept[0] == localStorage, localStorage is external)
1 true true

== is identity and there is no other equality, JavaScript’s own === for objects being exactly that. Two declarations of one path are one host value and compare equal; nothing else does.

print says <external Name> where the host offers a constructor name — <external Storage> for localStorage, <external ResizeObserver> for one of those — and <external> where it offers none. It is a debugging sentence and not a reading: nothing about a host object can be printed, and a program that wants a field’s value asks for the field.

What crosses the boundary

The invariant is one sentence: no foreign object ever becomes a slate value except as an external. It is the rule slate:dom already keeps by handing out integers, and everything in the two tables below follows from it.

Outward — a slate value handed to the host:

slateJavaScript
nullnull
a booleana boolean
an integera number, and one too large to be exact is a fault rather than a rounding
a reala number
a stringa string
an arraya new JavaScript array, each element crossing by this table
an objecta new plain object, each value crossing by this table
a functiona JavaScript function
a promisea JavaScript promise
an externalthe host value itself
anything elsea fault naming the kind

An array and an object are COPIED, and that is deliberate. slate’s array is a JavaScript array on this host and its object is a table the runtime owns, so handing either straight out would put slate’s own representation where the host can write to it — a fields map is not something an API can read, and a shared array is two languages mutating one thing under two sets of rules. A copy is one pass over what is being handed over anyway, and it keeps the boundary a boundary. The copy is not a snapshot the host keeps in step with: a change on either side after the call is not seen by the other.

A regular expression, a socket, a set, a map, a date, a duration and a class instance do not cross. Each is a slate value with an implementation the host has no reading of, and a fault naming the kind is worth more than a plausible-looking object with nothing behind it. What crosses instead is what the program means: the pattern’s text, the numbers a date holds, an array built from the set.

Inward — a host value handed back:

JavaScriptslate
undefinednull
nullnull
a booleana boolean
a numberan integer where it is whole, a real otherwise
a bigintan integer
a stringa string
a thenablea promise, whose value crosses by this table
anything elsean external

Everything that is not a primitive stays external, and that is the invariant read backwards. An array comes back external, an object comes back external, a function comes back external — read through the same property and call operations as anything else. There is no unwrapping and no conversion at the door, so nothing the host owns can be mistaken for something slate owns.

A whole number is an integer and a fractional one is a real, which is slate:dom‘s rule for its own property reads rather than a new one. JavaScript has one number and slate has two, so something has to decide, and the value is the only evidence there is — a length, a child count, a clientWidth are what a program reads off a host most often, and every one of them is a count a loop is about to be written over. A real there would make 0..<n a fault in the ordinary case.

undefined becomes null, and that is slate’s rule rather than a convenience. slate’s own undefined exists only as the immediate answer to a read that found nothing and may not be stored anywhere; a host property that is absent and one that holds undefined are the same read in JavaScript, so slate cannot honestly say which happened. null is a value a program can keep, and x.foo == null is the question to ask.

A thenable becomes a promise so that await works on it, which is the shape rule slate:gzip and fetch already follow — a surface that is asynchronous on one host is asynchronous everywhere, and a program written against it reads the same in both.

Handing a function out

A slate closure crosses as a JavaScript function, and its arguments cross INWARD by the table above. So a callback is handed exactly what the language guarantees: primitives as themselves, and everything else as an external it can read properties off. Its answer crosses outward.

It is given as many arguments as it declares and no more, which is the rule slate’s own natives already follow — map hands a callback the element and the index, and a one-parameter lambda takes it. JavaScript passes extra arguments everywhere, its own map calling back with three, so a strict count here would refuse the ordinary case. A callback declaring more than the host supplies is a fault, the function being right and what it was attached to being unable to feed it.

A method call keeps its receiver and a bare call keeps the one its path gave it. x.foo(a) calls foo with x as this; fetch(url) on an external fetch is called on the global object, because that is where the path found it. This is not a nicety — a browser’s fetch called on anything else throws Illegal invocation, and every method of a host object taken off it and called later has the same fault waiting in it. A function read out of an external as a property has no receiver: read it to hand it on, call it as a method to call it.

Under the interpreter

The declaration is accepted and every operation that would touch the host faults, naming the command:

external localStorage

print(localStorage.getItem("theme"))
no JavaScript host

==, is external, storing one and matching a name are not host operations and all work, which is what makes the two blocks above this section run at all. The line is exactly whether the host has to be asked: identity, travel and the type test are the language’s, and a property, a call and a construction are the host’s.

The module still exists and the import still succeeds, which is slate:dom‘s rule for the same reason: a refusal at the import is a complaint about the module and sends a reader looking for a spelling mistake in a line that is right. The mistake is the command, so the command is what the sentence names.

Reading and writing localStorage

Two properties and a method, and nothing else is needed. These blocks are for a page and are quoted rather than run, as slate:dom‘s are:

external localStorage

remember(key, value) = localStorage.setItem(key, value)

recall(key) = localStorage.getItem(key) ?? ""

theme() =
    val held = localStorage.getItem("theme")

    if held == null then "light" else held

getItem answers null for a key that is not there — the host’s own null, crossing as itself — and setItem takes two strings. An integer handed to it crosses as a number and JavaScript stringifies it, which is the host’s rule and not slate’s; a program that means a string writes one.

Watching an element with a ResizeObserver

The callback receives an external and reads a property off it, which is the shape nearly every observer API has:

import { byId } from slate:dom

external ResizeObserver

watch(id, onWidth)
    val seen = (entries, observer) ->
        val first = entries[0]
        val box = first.contentRect

        onWidth(box.width)

    val observer = ResizeObserver.new(seen)

    observer.observe(byId(id))

    observer

Four things in that function are the whole feature. ResizeObserver.new(seen) is JavaScript’s new with a slate closure as its argument; entries arrives as an external because a JavaScript array is not a slate array; entries[0] and first.contentRect are property reads answering further externals; and box.width is a number, so what reaches onWidth is an ordinary slate number that can be compared, stored and printed — an integer where the box is a whole number of pixels wide and a real where it is not, which is the rule for every number the host hands back.

observer is an external the caller keeps, and observer.disconnect() later is the same method call as any other. Nothing about the value is different for having been constructed here rather than read off a global.

What this is deliberately not

There is no type system for JavaScript here, and there is not going to be one. Kotlin/JS and Scala.js both let a program declare the shape of a foreign object, and both then have to keep two descriptions of one API in step by hand, with a compile that believes the declaration over the world. slate’s answer is that the host’s values are the host’s: the checker knows an external’s kind and nothing whatever about its members, so x.anything is never refused and never promised.

x.new(...) is the one name an external cannot reach as a method, new being how construction is spelled. It is the same word slate uses for its own classes, which is what makes it worth the cost.

external does not import anything. A path names something the host has already loaded — a page’s globals, a module the program was bundled with — and how a name comes to be on the global object is the host’s business, not slate’s.

Search

Esc
to navigate to open Esc to close