Types in practice
Every value in Dendrite has a type, and the checker runs before anything else does. A string where a number belongs is a message with a line and a column, not a surprise halfway through an evaluation.
That is an op_input_type_mismatch. The program above does not run at all.
The types you will meet
Section titled “The types you will meet”Four primitives, written as you would guess: number, string, boolean,
and the two that behave specially, any and null.
Lists are written with [] after the element type, so number[] is a list of
numbers and string[][] a list of lists of strings. Functions are written with an arrow:
(number) -> boolean is what Filter wants for its predicate. Neither of those is
registered anywhere: they are built out of other types as you need them, which is why there is
no list of “all the array types”.
An application can add named types of its own, with fields and with a parent type they extend. Those arrive alongside the inputs that use them, and the editor shows you what they hold.
A list knows what is in it
Section titled “A list knows what is in it”You never write the element type of a literal list. It is read off the contents:
let numbers = [4, 8, 15]
output total = Average(numbers)numbers is a number[], so Average accepts it. Hand it a list of strings
and it will not.
A list of mixed contents falls back to any[], which still works but tells the checker
less, and you will notice the difference the first time you pass it somewhere fussy.
any accepts anything, and asks nothing
Section titled “any accepts anything, and asks nothing”any is the type an application uses when it genuinely does not know what a value will
be. A value of any data type flows into it, and a value of type any flows into any data type,
which means:
That compiles, with a warning where the any meets Length, and the warning names
what you wrote: '$whatever' is 'any' typed. Here $whatever holds a list, so length is 3. Type a number into it instead and nothing stops it:
length shows 0, a plausible number that is simply wrong, because the one check that
would have caught it is the one any gave up. (A list op reads anything that is not a list
as an empty one, so the program keeps running; what it cannot do is tell you the value was never
a list.)
So any is an escape hatch with a cost: you trade the check for the flexibility. When you see
an implicit_any_cast warning, that is the checker telling you where the trade happened,
in case you did not mean it.
The one thing any will not accept is a function. That single exception is what keeps the
language total, and it is explained in
the type system under How it works.
null goes anywhere
Section titled “null goes anywhere”An input with no value yet holds null, and null is accepted wherever a data value is
expected. A program over unset inputs therefore still compiles and still runs, rather than
refusing to start.
It will not produce sensible numbers, which is what Default and IsSet are for -
see Inputs. A list op reads a null as an empty list, so Length of an
unset list is 0 and Find over one is null, and a field of null is null
again, so Find(buses, b => b.id == 9).name is simply unset when no bus matches. Nothing
throws.
Converting on purpose
Section titled “Converting on purpose”Nothing converts on its own. A number is never read as a boolean, and text that happens to hold digits is still text, so the checker’s answer always means what it says. When you do want the other type, you say so, where you want it:
let celsius = Default(ToNumber($reading), 0)
output label = ToString(celsius * 2)
output hot = ToBool(celsius)ToNumber reads text as a number, and gives null when it is not one: "n/a", the
empty string, "12 degrees". That is on purpose. A 0 would be a guess you could not tell from a
real zero, so the fallback is yours to write, with the Default you already know.
ToBool is false for false, 0, the empty string, null and an empty
list, and true for everything else. The three are on the
conversion page.
One op converts for you. Join turns each of its parts into text itself, which its
signature marks as parts~, and that is why a template (`n = {count}`, on the
operators page) needs no ToString in its holes.
Stating a type
Section titled “Stating a type”A binding can say what type it holds:
let rows: number[] = $rows
output mean = Average(rows)rows is a number[] to everything that reads it, so Average sees the list it
wants. The annotation is a claim, not a check: $rows is any, so the checker still
warns where the claim is made, and the message says what would turn the claim into a check:
Use 'as number[]' to check it. A value the claim cannot hold at all is an error,
binding_type_mismatch, and a type name the language does not have is unknown_type.
Where an annotation earns its keep is the empty list. [] is a list of nothing in
particular, any[], and a name bound to it warns the moment it reaches a typed input.
Stated, it is quiet and precise:
let none: number[] = []
output mean = Average(none)Checking a type
Section titled “Checking a type”as checks a value against a type while the program runs:
let rows = $rows as number[]
output mean = Average(rows)
output known = IsSet(rows)rows is the list when $rows holds a list of numbers, and null when it holds
anything else: a number, a list with text in it, nothing at all. That is the answer ToNumber
gives for text that is not a number, for the same reason. null means “no value”, the list
ops read it as an empty list, IsSet sees it, and Default(rows, []) is the fallback you
write yourself. There is no warning, because nothing is left to warn about: the value was checked.
A cast never throws and never changes a value. "5" as number is null, not 5. When
the checker can already see that no value could fit, as there, it says so with
cast_never_fits, a warning, since the cast is pointless rather than wrong. A cast to a
function type is an error: a function cannot be checked. as binds tighter than any symbol,
so 1 + $x as number casts $x alone, and $row.value as number casts the field.
Reading a message
Section titled “Reading a message”Diagnostics come in two severities and they mean different things.
- An error stops the output that depends on it. The rest of the program still runs: this language checks each output on its own, so one broken result does not take the others with it.
- A warning changes nothing. It is telling you something is pointless or lossy: a binding
nothing reads, an op input that does not exist, a value going into
any.
Every message names its kind, and every kind has an entry with an example that triggers it in the diagnostics catalogue. When you meet one you do not recognise, that is where it is written down. picture.