Skip to content

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.

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.

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 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.

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.

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.

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)

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.

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.