Standard library
Everything callable an author did not declare, and which declaration kinds may call each one.
The callable surface is small on purpose, and this is the whole of it. A method that is not in these tables does not exist, and the error names the receiver.
There are no modules and no namespacing. The surface is methods keyed by receiver type, plus constructors named by the type they build.
Methods by receiver
String
| Method | Returns | |
|---|---|---|
trim(), lower(), upper() |
String |
|
len() |
Int |
|
is_empty() |
Bool |
|
contains(s), starts_with(s) |
Bool |
|
strip_prefix(s) |
String |
unchanged when the prefix is absent |
after_last(s) |
String |
the whole string when the separator is absent or empty |
to_int() |
Int? |
|
to_uuid() |
Uuid? |
strip_prefix is written after a starts_with that already decided, and after_last is
total so gid.after_last("/") is safe on something that is not a global id. The two
conversions are optional because there the failure is the point.
Json
| Method | Returns |
|---|---|
string(key) |
String? |
int(key) |
Int? |
bool(key) |
Bool? |
json(key) |
Json? |
array(key) |
List(Json)? |
number(key) |
String?, the exact text of a JSON number |
Every one is optional: a missing key and a key holding the wrong shape both answer none,
so you write one branch rather than two. int answers none for 10.5.
number returns text because a Decimal or a Money needs a scale, and the scale belongs
to the target rather than to the digits on the wire.
There is no get, no dynamic field access and no indexing.
T?
| Method | Returns |
|---|---|
unwrap_or(T) |
T |
is_some(), is_none() |
Bool |
Three, and there is no unwrap and no expect. Narrowing removes most of the calls you
would otherwise write.
List(T)
| Method | Returns |
|---|---|
first() |
T? |
push(T) |
List(T) |
remove(T) |
List(T) |
contains(T) |
Bool |
len() |
Int |
is_empty() |
Bool |
push and remove build a new list rather than mutating one. remove removes every
equal element, so it is idempotent. There is no indexing.
Map(K, V)
| Method | Returns |
|---|---|
get(K) |
V? |
set(K, V) |
Map(K, V) |
remove(K) |
Map(K, V) |
contains(K) |
Bool |
keys() |
List(K) |
values() |
List(V) |
len() |
Int |
is_empty() |
Bool |
keys() and values() come back sorted by key, which is load-bearing for rebuild and
replay determinism.
Money(n)
| Method | Returns |
|---|---|
mul(Decimal(s), Rounding) |
Money(n) |
div(Int, Rounding) |
Money(n) |
The two places money is allowed to round, and the author says how. Rounding is HalfUp,
HalfEven or Down, written as a bare word.
The result keeps the amount’s scale; the rate’s own scale is whatever you wrote, so
total.mul(0.9, HalfUp) takes a Decimal(1).
Timestamp
year(), month(), day(), hour(), minute(), second(), each an Int, each in UTC.
There is no add, no duration type and no format. Calendar arithmetic is written as a
fn, because month-end clamping is one opinion among several and the language should not
pick yours.
Outcome
| Method | Returns |
|---|---|
ok() |
Bool |
code(), message() |
String? |
refused(<RefusalName>) |
Bool |
refused declares a String, and a bare refusal name resolves to its code there, so writing
the name makes a rename a compile error rather than a branch that stops matching. A literal
is accepted too, and nothing checks it. An invalid carries no code, so it is refused by
nothing.
Response
.status is an Int and .body is a Json, without parentheses. This is the only
parenless field access on a builtin type in the language.
Constructors
The global namespace is closed: anything built from nothing is named by its type.
| Call | Returns | |
|---|---|---|
Uuid.derive(seed, name) |
Uuid |
a pure function of both arguments |
Json.empty |
Json |
|
Json.encode(value) |
String |
|
Map.empty |
Map(K, V) |
the type comes from the target |
Timestamp.parse(text) |
Timestamp? |
RFC 3339, offset required |
Timestamp.from_parts(y, mo, d, h, mi, s) |
Timestamp? |
optional, because Feb 30 is not a date |
Money.parse(text) |
Money(n)? |
the scale comes from where the result lands |
Decimal.parse(text) |
Decimal(n)? |
the same, for a rate |
reject <Name> |
Outcome |
a declared refusal, as a value |
invalid(message) |
Outcome |
the same, for a malformed request |
There is no List.empty, because [] already writes it, and no Map literal, because
{ ... } is a JSON object.
Money.parse widens exactly: "10.5" into Money(3) is 10.500, and "1.2345" into
Money(3) is none.
There is no uuid4() and no random. Uuid.derive is the whole identity story, and it
is pure in both arguments, so a retry and a replay produce the same id.
What a handler does
| Call | Returns | Journaled |
|---|---|---|
http.get(url) |
Response |
yes |
http.post(url, body), .put, .patch |
Response |
yes |
http.delete(url) |
Response |
yes |
invoke Name { ... } |
Outcome |
yes |
now() |
Timestamp |
yes, pinned once per invocation |
erase(value) / erase(subject, value) |
nothing | yes |
log(message) |
nothing | no |
fail(message) |
nothing, terminal | n/a |
reveal(value) |
the sealed type, unsealed | no, re-decrypts every attempt |
Every http verb takes an optional trailing headers = { ... }. There is no timeout clause
and no timeout key: one attempt is capped at 10 s to connect and 30 s overall, both fixed in
the runtime.
Where each may be called
The pure surface above is available everywhere. What reaches the world is not.
| command | guard | projector | effect arm | effect fn |
module fn |
fold arm | |
|---|---|---|---|---|---|---|---|
now() |
yes | no | no | yes | no | no | no |
http.* |
no | no | no | yes | yes | no | no |
invoke |
no | no | no | yes | yes | no | no |
log, fail |
no | no | no | yes | yes | no | no |
reveal, erase |
no | no | no | yes | no | no | no |
emit |
yes | no | no | no | no | no | no |
put/patch/update/delete |
no | no | yes | no | no | no | no |
reject, invalid |
yes | yes | no | no | no | with -> Outcome |
no |
Four rules do all of that work:
- A projector has no clock and no network, because a rebuild has to reproduce every value it wrote.
- A command has a clock but cannot call out, because only an effect journals a call.
- A module
fnis pure, which is the whole of what makes it callable from a command, a projector and a fold arm alike. - An effect-local
fnmay call out but not decrypt and not read a clock, becausenow()is pinned into a slot the arm fills, andrevealanderasestay in the arm for the erase-last analysis.
No fold arm calls any of them. A fold is the definition of a state variable, and a definition that reached the network would be a different value on every read.
A test calls none of them. It states inputs and expectations; given, respond and
erased script the world instead.
Deliberately absent
Each of these is a decision rather than a gap.
- No
+on strings, nostr(), no.to_string(), no format specifiers. Interpolation is the whole mechanism and the JSON text table is the whole text form. - No
sort,map,filterorfoldmethods. A comprehension covers map and filter, iteration order is already defined, and a fold over a container is aforinside a purefn. - No set type and no tuple type.
Map(K, Bool)covers membership and a record covers two values that travel together. - No
x.expect("reason").unwrap_orand narrowing cover it without a panic. - No random, no
uuid4, no minted identity. - No duration type and no timestamp arithmetic. The calendar fields plus
from_partsmake it writable as afn, which is where the clamping rule belongs. - No regular expressions.
contains,starts_withandafter_lastcover what came up. - No
Moneyconversion and no rate type. Both need currency back in the type, and currency is deliberately not in the type. Declare an ordinary field beside the amount. - No
while, nobreak, nocontinue, no recursion, no mutable bindings, no closures, no generics, no overloading, and no default or namedfnarguments.