The prelude

The prelude is the package whose public names are in scope in every file without an import, and it holds only the pure part of the standard library.

A new file already has Option, List, print and every other name of the glossary in scope. use is for everything else: the prelude is what a project decided nobody should have to import.

Example#

const numbers = [1, 2, 3]
print numbers.map({ _ * 2 }).toList()

Nothing here is imported: List, the closure syntax, map and print are all part of std/prelude.

Syntax#

use <Name> from "std/prelude"    // Legal, but redundant for a name the prelude already exports

Rules#

  1. std/prelude declares nothing of its own. Its src/lib.trb is nothing but public use ... from "std/..." lines, so every name it re-exports can also be imported straight from the package that declares it.

    That includes members other packages attach to a type: public use Int64.seconds from "std/time" is why 2.seconds() works everywhere, although std/time does not own Int64. See extend for why such a member is named at all, and use for the form.

    print 2.seconds()
  2. A project's prelude is std/prelude unless project.trb names another one. The field is prelude on Project, so a teaching subset or the vocabulary of an embedded DSL can replace it for a whole project.

  3. The prelude holds the pure part of the standard library, and none of its capabilities. Values, text, numbers, collections, pipelines, encoding, quotations, tasks and printing are in scope everywhere; std/fs, std/os, std/process, std/io, std/http, std/sandbox and Clock are not.

    Does not compile
    fn checkExists(path: String): Bool {
      File.exists path
    }
    // error: Cannot find `File` here
  4. An import of a capability is a statement a reviewer can read at the top of the file. use File from "std/fs" says "this file touches files" before a single line of its body does, which is exactly what a name that was already in scope could never say.

  5. A sandbox gives its script the prelude of the host plus the receiver, and nothing else. A receiver script is defined that way, which is why the split between the prelude and every capability package is what makes the sandbox possible at all.

  6. The prelude ranks below everything a file wrote or imported. A name the file declares or imports - under an as name too, and a case it imports - shadows the prelude's name of that spelling. For a member, what the file wrote or named itself answers before what it reaches only through its prelude, whatever the specificity of either: a file's own extend<Item> Iterate<Item> member beats a List member the prelude re-exports, and a use String.shout from "acme/text" beats a String.shout of the prelude. Two members of the same rank are still the ambiguity at the use (see use). So the prelude can grow in a minor release: a name, member or trait it gains never breaks a program and never changes what it means.

What this is not#

The prelude is not "everything the standard library has". A name that is not re-exported by std/prelude needs its own use, however small it looks.

use Clock from "std/time"

const start = Clock.now()
const elapsed: Duration = Clock.now() - start
print elapsed
Does not compile
const start = Clock.now()
// error: Cannot find `Clock` here

Instant, Duration, Timestamp and ByteSize need no import - they are values, and every field of a program can hold one. Clock reads the clocks of the machine, which is a capability, and only an explicit use brings it into scope. Path is the same split: a path is a value that names a file and needs no import, and File, which opens what it names, comes from std/fs.

const config = Path.from "project.trb"
print config.extension()
  • std/prelude - every name this package re-exports today.
  • use - every form an import can take.
  • Packages - what a package is, and how the prelude is one.
  • Visibility - public, which is what makes a name re-exportable at all.
Esc