User Functions are functional expressions, defined by the user. (They could also be called "lambdas".) User functions are helpful when the user needs to apply some repetitive action, or to pass an action to be applied to each item in an array.

A User Function contains a list of parameters and then an expression that is calculated for these parameters.

A User Function is a type of value, so you can assign a user function to a local variable, or pass it to some higher-order function as a parameter.

There are three ways to define a user function.

Functional Expression

A functional expression is the canonical form for user functions. It contains a list of parameters in parentheses, followed by the "maps to" symbol (->), followed by the expression calculated by the function. When there's only one parameter, the parentheses can be omitted.


Examples:

  • () -> START_OF_MONTH(NOW())
  • (x) -> x * x
  • version -> version.releaseDate - version.startDate

  • (s1, s2) -> s1 CONCAT " " CONCAT s2

All these examples evaluate to a "User Function" value type, which can be assigned to a variable:

  • WITH square = x -> x * x : ...

Traditional Function Definition

A more traditional function definition looks similar to a variable definition, only the variable is followed by a list of parameters in parentheses, and the expression is based on those parameters.

To rewrite the examples above:

  • WITH currentMonth() = START_OF_MONTH(NOW()) : ...
  • WITH square(x) = x * x : ...
  • WITH versionDuration(version) = version.releaseDate - version.startDate : ...

  • WITH joined(s1, s2) = s1 CONCAT " " CONCAT s2 : ...

These declarations are identical to the corresponding examples in the previous section, with local variables assigned to the corresponding User Function values.

Implicit Functional Expression ($)

Most of the time when we're creating a formula with an array, we need to apply some kind of operation to each element of the array. Implicit functional expressions help define the corresponding user function easily by having "$" denote "each element".

For example:

  • versions.FILTER($.startDate < NOW())
  • issueLinks.FILTER($.type = "Relates").MAP($.destination)

  • worklogs.UMAX_BY(IF $.author = ME() : $.timeSpent)

In each case, the expression with "$" is transformed into a User Function with a single parameter, which is then substituted for $. So, the last example from the list above is identical to:

  • worklogs.UMAX_BY(w -> IF w.author = ME() : w.timeSpent)

When reading these expressions, you can say "each" when the dollar sign is encountered.

An implicit user function must always be used in an argument to a system function, which expects a user function. Otherwise, it won't be accepted.

For example – here's how we can filter an array to contain only even numbers:

CorrectIncorrect – Parse Error

ARRAY(1, 2, 3).FILTER(MOD($, 2) = 0)

... or, alternatively ...

WITH even(e) = MOD(e, 2) = 0 :
ARRAY(1, 2, 3).FILTER(even)

WITH even = MOD($, 2) = 0 :
ARRAY(1, 2, 3).FILTER(even)

Calling User Functions

If a User Function is assigned to a variable, you can call it in your expression in the same way you call a system function.

  • WITH square(x) = x * x :
    square(impact) / square(cost)

You can also use the chained function call notation:

  • WITH square(x) = x * x :
    WITH fquare(x) = x.square().square() : 
    storyPoints.fquare()

Note that you cannot invoke a functional expression unless it is assigned to a local variable. The following will produce an error: (x -> x * x)(3)

 Function Name Collisions

Both system functions and user functions are invoked in the same way – FUNCTION_NAME(arg1, arg2, arg3, ...), or with a chained call syntax – arg1.FUNCTION_NAME(arg2, arg3, ...). This leaves a potential for the user to define a function that has the same name as a system function.

(warning) When Expr encounters a function call, first it looks up if there is a system function of that name. (warning)

The system function will be called even if there's a local variable of the same name. To protect the user from name collisions, Expr will show an error if you try to define a function with a name that matches a system function name. (However, it won't be able to detect the collision if a local variable is defined through a series of assignments of a functional expression.)

You can define a local variable of any other type with a name identical to a system function's name.

Works as expectedError
// Using "SUM" as a local variable,
// but "SUM()" is also a system function
// and "SUM{}" is an aggregate function.
WITH SUM = cost + parent.cost :
SUM(SUM, SUM { cost })
CODE
// Cannot define a user function with a name collision.
// Note that the language is case-insensitive: "SUM" and "sum" are the same.
WITH sum(issue) = 
   issue.timeSpent + issue.parent.timeSpent :
...
CODE

Note that the function name collision resolution provides potential challenges when upgrading to a newer version of Structure, if that version introduces new system functions.

Let's say you have defined a user function LAST_COMMENT() in your formula and used it successfully in an older Structure version. If the newer version of Structure adds a system function LAST_COMMENT(), that formula will likely stop working after the upgrade, and you will need to rename the user function.

To minimize the probability of this happening, we suggest naming your user function in a way that makes potential collision unlikely. It could be a name that is very specific to your configuration, or you can always prepend the name with an underscore – in our example, call it _LAST_COMMENT().