Skip to content

The standard library

These pages are for writing programs. Building on the library, or replacing parts of it, is under Host developers.

The standard library is a language: the primitive types, the ops every application has, and the symbols that are sugar over them. It is what you get before your application adds anything of its own.

The pages beside this one are generated from the library itself, one per segment, each op with its real signature and an example that was run to produce the value shown. They cannot drift from the code, because they are read out of it at build time.

This page is the part that is not generated: the conventions all of them assume.

Every entry leads with one, and it is the real one, printed from the op:

Filter(list: any[], predicate: (any) -> boolean) -> any[]

The name, then each input with its type, then what comes out. Those input names are the ones you use when you pass arguments by name rather than in order, which Operators and symbols covers. The rest of this page is the parts of a signature that are not obvious on sight.

A signature ending in ... takes as many values as you give it:

output sum   = Add(1, 2, 3, 4)
output any   = Or($a, $b, $c)
output joined = Concat([1, 2], [3], [4, 5])

Add(nodes...: number) means one input called nodes that swallows every argument. Six ops work this way: And, Or, Xor, Add, Multiply and Concat.

The symbol form of a variadic op takes two at a time, so 1 + 2 + 3 nests where Add(1, 2, 3) does not. Same answer; the op form says “sum these” more directly.

An input marked ? may be left out: Join(parts~: string[], separator?: string) joins with nothing between the parts when it gets no separator. One op has one so far. Leaving out any other input is a missing_op_input warning, with the type’s default standing in.

An input marked ~ converts what it is given. parts~: string[] accepts a list of anything, and each value in it becomes text before Join runs, so Join([1, 2], ", ") is "1, 2" with no ToString and no warning. The conversion is the op’s, declared on that one input and written under its signature; nothing else in the language converts on its own, and Join is the only op in the library that does. A host can declare the mark on an op of its own with convert: true, and a lambda can carry it on a parameter, (t~: string) => Upper(t) (Naming a function). The shape still has to match: Join(5) is a type error.

An input typed any accepts any data value. Length(list: any[]) takes a list of anything, Equals(a: any, b: any) compares anything with anything.

This is not the op being careless. It is the op saying it does not care, which is true: Length counts without looking inside. What you lose is the check. Pass Length a list of strings and nothing complains, because nothing needed to.

Where an op can be more precise, it is, which is the next convention.

A signature is the general shape. Several ops narrow it for the particular call:

let numbers = [4, 8, 15]
output kept = Filter(numbers, n => n > 10)

Filter’s signature says -> any[], but this call produces a number[], because the list it was given was one. The same holds for Map (from its function’s return type), Find (the element type), Reduce (from the starting value) and If, which gives you the branch type when both branches agree and any when they do not.

So the reference tells you what an op accepts, and the editor tells you what your call produced. When they differ, the editor is right.

An input written (number) -> boolean wants a function. Six list ops have one, and between them they are how iteration is expressed:

let items = [4, 8, 15, 16, 23]
output big = Filter(items, item => item > 10)

There is nothing special about those ops. A function is a value in this language, so an op taking one is an ordinary op, and you can hand it a lambda written in place or a name bound earlier. What the signature does not show is that the parameter type is worked out for you: the item above is a number because items is a list of numbers, so you rarely need to annotate it.

Lambdas and lists covers writing them.

The ops are grouped, and the grouping is real: it is a field on each op, which is what splits these pages up.

SegmentWhat is in it
logicAnd, Or, Xor, Not
comparisonEquals, NotEquals, LessThan, GreaterThan
controlIf, Default, IsSet
arrayLength, Concat, Includes, Average, Min, Max, Flatten
arithmeticAdd, Subtract, Multiply, Divide
listFilter, Map, Reduce, Find, Some, Every
conversionToString, ToNumber, ToBool
stringJoin, Upper, Lower, Trim, Contains, StartsWith, EndsWith

Today an application takes the whole library or none of it. The segments are the seam along which that will change, so that an application which never touches a list need not carry the list ops, and these pages can tell you which segments your application has. Until then, if you are reading this inside somebody’s product, assume all of them and check the editor’s own lists for anything extra.

Everything here is the floor, not the ceiling. An application embedding Dendrite registers its own types and its own ops, and those behave exactly like these: same call syntax, same checking, same reference format if it generates one. You cannot tell from a program whether Filter came from the standard library or from the application, and that is the point. what happens to a program that uses them.