Skip to content

Lambdas and lists

Dendrite has no loop. It does not need one, because a function is a value you can hand to an op.

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

item => item > 10 is a lambda: a function of one argument. Filter is an ordinary op that happens to take one, the same way Add takes numbers. There is no special syntax for iteration and no special node kind in the compiled program.

Six of them, and between them they cover what a loop would have done:

let items = [4, 8, 15, 16, 23]

output kept    = Filter(items, n => n > 10)
output doubled = Map(items, n => n * 2)
output total   = Reduce(items, 0, (sum, n) => sum + n)
output first   = Find(items, n => n > 10)
output anyBig  = Some(items, n => n > 10)
output allBig  = Every(items, n => n > 10)

Reduce is the one to look at twice. It takes the list, a starting value, and a function of two arguments called with the accumulator first and the item second, so (sum, n) => sum + n, in that order.

Beside these live the ops that do not need a function at all: Length, Concat, Includes, Average, Min, Max, Flatten. Reach for those first; a lambda you do not need is a lambda someone has to read.

Text is built the same way everything else is here, from a list. Join takes the parts and, if you give it one, what goes between them:

let sources = ["cam 1", "cam 2", "mic"]

output label  = Join(Map(sources, name => Upper(name)), ", ")
output packed = Join(sources)
output status = Join(["Live: ", Length(sources)])

label is "CAM 1, CAM 2, MIC", and packed runs the parts together because it has no separator. status is where a number goes into a sentence, and it works without a ToString: Join is the one op that converts its parts to text itself, which its signature shows as parts~. Nothing else converts on its own. The rest of the text ops (Trim, Contains and their neighbours) are on the string page.

A lambda can use anything in scope where you wrote it, inputs included:

height => height >= $threshold captures $threshold. Change the threshold and the lambda behaves differently, because the closure holds the input rather than a copy of its value. This is lexical scope, working the way you expect it to.

A lambda’s parameter shadows an outer name of the same name, and only inside the lambda:

The n inside is the item, 1 then 2 then 3. The n outside is still 100, and is now unused: the unused_binding warning on it is the analyser saying so. The shadowing itself is allowed, and raises nothing.

A lambda is a value, so it can be bound like any other and applied later:

let double = (n: number) => n * 2
output four = double(2)
output list = Map([1, 2, 3], double)

Give a parameter a type when you want to be explicit, and pass an argument by its parameter’s name when that reads better:

let adjust = (x: number) => x + 10
output named = adjust(x: 5)

A parameter can also convert what it is given. Mark it ~, the mark Join carries on its parts~, and write the type to convert to:

let shout = (t~: string) => Upper(t)
output one  = shout(5)
output many = Map([1, true], shout)

shout accepts anything, and t is text by the time the body reads it: one is "5" and many is ["1", "TRUE"]. Without the mark Upper would refuse a number, and every call would need its own ToString. The type has to be written, and it has to be string, number, boolean or a list of one of them, the types the conversion ops cover. Anything else, or the mark with no type, is invalid_convert_param.

A function cannot call itself, and the language has no way to express it. A name referring to itself is a binding_cycle, and self-application cannot be given a type, because a function is never accepted where any is expected.

That is deliberate, and it is the reason every Dendrite program terminates. There is no program a user can write that loops forever inside your application. You pay for it by expressing iteration through the ops above rather than by writing your own recursion, which for the work this language is for is a trade worth making.