Skip to main content

Functions and Lambdas

Functions do the work in most TransformScript transforms. There is one flat library of built-in functions with no modules to import. You can call any function in two equivalent ways, you can pass lambdas (anonymous functions) to the functions that accept them, and a $ shorthand keeps the common cases short.

Calling Functions

You can write a function call two ways, and they mean the same thing:

upper(payload.name)            // prefix form:  fn(receiver, …)
payload.name upper()           // postfix form: receiver fn(…)

The postfix form takes the value on its left and passes it as the first argument, so receiver fn(a, b) is the same as fn(receiver, a, b). This is what lets transforms read left to right:

payload.items filter (i) -> i.active map (i) -> i.name
// same as map(filter(payload.items, (i) -> i.active), (i) -> i.name)

There is no .method() chaining and no pipe operator. Postfix calls are the chaining mechanism. When you want to thread a whole value into an arbitrary expression, use then (see Threading With then).

Defining Functions

You define named functions in the header with fun:

%files 1.0
output json
fun discount(price, pct) = price - (price * pct / 100)
fun label(p) = "$$(p.sku): $(p.name)"
---
discount(payload.price, 10)

A fun is in scope for the whole body and for other funs. You can also introduce local functions with do { fun … --- body }.

Lambdas

A lambda (anonymous function) is (params) -> body. With exactly one parameter, the parentheses are optional:

(row) -> row.total
x -> x * 2
(a, b) -> a + b
() -> 42

Parameters can declare defaults and type annotations:

(x, factor = 2) -> x * factor
(row: Object) -> row.id

A lambda closes over the bindings in scope where you write it, so it is a true closure. A lambda checks arity at call time, and passing fewer than the required (non-defaulted) parameters raises an error.

You pass lambdas to the higher-order functions such as map, filter, reduce, and groupBy:

payload.orders map (o) -> { id: o.id, net: o.total - o.tax }
payload.nums filter (n) -> n mod 2 == 0
payload.nums reduce (acc, n) -> acc + n

The $ Shorthand

In the lambda-argument position of a function, you can drop the (param) -> and reference arguments positionally with $, $$, and $$$, using one $ per parameter:

payload.nums map $ * 2              // same as map(payload.nums, (a) -> a * 2)
payload.nums filter $ > 10          // first arg
payload.kv mapObject "$$": $        // $ = value, $$ = key  (2nd arg)
payload.nums reduce $ + $$          // $ = item, $$ = accumulator

The number of $ characters is the parameter index: $ is argument 1, $$ is argument 2, and $$$ is argument 3. The shorthand applies only where a function expects a lambda, so a bare $ anywhere else is just a variable named $.

Many functions take a lambda, so the $ shorthand works with them. These functions are:

avg, countBy, countCharactersBy, distinctBy, dropWhile, every, everyCharacter, everyEntry, filter, filterObject, firstWith, flatMap, groupBy, indexWhere, join, leftJoin, map, mapObject, mapString, maxBy, mergeWith, minBy, onNull, orderBy, outerJoin, partition, pluck, reduce, scan, some, someCharacter, someEntry, splitWhere, substringBy, sum, sumBy, takeWhile, then, with

Note that map's lambda also receives the index as its second parameter, so payload.items map (item, idx) -> … (or $ / $$) works.

Threading With then

Because there is no pipe operator, you thread a value into an arbitrary expression with then(value, lambda). It calls the lambda with the value and returns the result:

payload.items
  filter (i) -> i.active
  then (active) -> { count: sizeOf(active), names: active map $.name }

then is the idiomatic way to compute one intermediate result and then build a larger structure from it without a header var.